Initial commit of new Canvas operation recording / replay

Done:
- drawRect, drawBitmap, drawColor, drawPaint, drawRenderNode, drawRegion
- Recording with new DisplayList format
- batching & reordering
- Stateless op reorder
- Stateless op rendering
- Frame lifecycle (clear, geterror, cleanup)

Not done:
- SaveLayer (clipped and unclipped)
- HW layers
- Complex clipping
- Ripple projection
- Z reordering
- Z shadows
- onDefer prefetching (text + task kickoff)
- round rect clip
- linear allocation for std collections
- AssetAtlas support

Change-Id: Iaf98c1a3aeab5fa47cc8f9c6d964420abc0e7691
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index 8011b59..6f548bc 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -2,6 +2,8 @@
 include $(CLEAR_VARS)
 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
 
+HWUI_NEW_OPS := false
+
 hwui_src_files := \
     font/CacheTexture.cpp \
     font/Font.cpp \
@@ -85,6 +87,17 @@
     -Wall -Wno-unused-parameter -Wunreachable-code \
     -ffast-math -O3 -Werror
 
+
+ifeq (true, $(HWUI_NEW_OPS))
+    hwui_src_files += \
+        BakedOpRenderer.cpp \
+        OpReorderer.cpp \
+        RecordingCanvas.cpp
+
+    hwui_cflags += -DHWUI_NEW_OPS
+
+endif
+
 ifndef HWUI_COMPILE_SYMBOLS
     hwui_cflags += -fvisibility=hidden
 endif
@@ -172,6 +185,13 @@
     unit_tests/LinearAllocatorTests.cpp \
     unit_tests/StringUtilsTests.cpp
 
+ifeq (true, $(HWUI_NEW_OPS))
+    LOCAL_SRC_FILES += \
+        unit_tests/BakedOpStateTests.cpp \
+        unit_tests/RecordingCanvasTests.cpp \
+        unit_tests/OpReordererTests.cpp
+endif
+
 include $(BUILD_NATIVE_TEST)
 
 # ------------------------
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
new file mode 100644
index 0000000..4d9f9b4
--- /dev/null
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "BakedOpRenderer.h"
+
+#include "Caches.h"
+#include "Glop.h"
+#include "GlopBuilder.h"
+#include "renderstate/RenderState.h"
+#include "utils/GLUtils.h"
+
+namespace android {
+namespace uirenderer {
+
+Texture* BakedOpRenderer::Info::getTexture(const SkBitmap* bitmap) {
+    Texture* texture = renderState.assetAtlas().getEntryTexture(bitmap);
+    if (!texture) {
+        return caches.textureCache.get(bitmap);
+    }
+    return texture;
+}
+
+void BakedOpRenderer::Info::renderGlop(const BakedOpState& state, const Glop& glop) {
+    bool useScissor = state.computedState.clipSideFlags != OpClipSideFlags::None;
+    renderState.scissor().setEnabled(useScissor);
+    if (useScissor) {
+        const Rect& clip = state.computedState.clipRect;
+        renderState.scissor().set(clip.left, viewportHeight - clip.bottom,
+            clip.getWidth(), clip.getHeight());
+    }
+    renderState.render(glop, orthoMatrix);
+    didDraw = true;
+}
+
+void BakedOpRenderer::startFrame(Info& info) {
+    info.renderState.setViewport(info.viewportWidth, info.viewportHeight);
+    info.renderState.blend().syncEnabled();
+    Caches::getInstance().clearGarbage();
+
+    if (!info.opaque) {
+        // TODO: partial invalidate!
+        info.renderState.scissor().setEnabled(false);
+        glClear(GL_COLOR_BUFFER_BIT);
+        info.didDraw = true;
+    }
+}
+void BakedOpRenderer::endFrame(Info& info) {
+    info.caches.pathCache.trim();
+    info.caches.tessellationCache.trim();
+
+#if DEBUG_OPENGL
+    GLUtils::dumpGLErrors();
+#endif
+
+#if DEBUG_MEMORY_USAGE
+    info.caches.dumpMemoryUsage();
+#else
+    if (Properties::debugLevel & kDebugMemory) {
+        info.caches.dumpMemoryUsage();
+    }
+#endif
+}
+
+void BakedOpRenderer::onRenderNodeOp(Info*, const RenderNodeOp&, const BakedOpState&) {
+    LOG_ALWAYS_FATAL("unsupported operation");
+}
+
+void BakedOpRenderer::onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
+    info->caches.textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
+    Texture* texture = info->getTexture(op.bitmap);
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    const int textureFillFlags = (op.bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
+    Glop glop;
+    GlopBuilder(info->renderState, info->caches, &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedUnitQuad(texture->uvMapper)
+            .setFillTexturePaint(*texture, textureFillFlags, op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRectSnap(Rect(0, 0, texture->width, texture->height))
+            .build();
+    info->renderGlop(state, glop);
+}
+
+void BakedOpRenderer::onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
+    Glop glop;
+    GlopBuilder(info->renderState, info->caches, &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshUnitQuad()
+            .setFillPaint(*op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRect(op.unmappedBounds)
+            .build();
+    info->renderGlop(state, glop);
+}
+
+void BakedOpRenderer::onSimpleRectsOp(Info* info, const SimpleRectsOp& op, const BakedOpState& state) {
+    Glop glop;
+    GlopBuilder(info->renderState, info->caches, &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshIndexedQuads(&op.vertices[0], op.vertexCount / 4)
+            .setFillPaint(*op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewOffsetRect(0, 0, op.unmappedBounds)
+            .build();
+    info->renderGlop(state, glop);
+}
+
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
new file mode 100644
index 0000000..b8b4426
--- /dev/null
+++ b/libs/hwui/BakedOpRenderer.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_BAKED_OP_RENDERER_H
+#define ANDROID_HWUI_BAKED_OP_RENDERER_H
+
+#include "BakedOpState.h"
+#include "Matrix.h"
+
+namespace android {
+namespace uirenderer {
+
+class Caches;
+struct Glop;
+class RenderState;
+
+class BakedOpRenderer {
+public:
+    class Info {
+    public:
+        Info(Caches& caches, RenderState& renderState, int viewportWidth, int viewportHeight, bool opaque)
+                : renderState(renderState)
+                , caches(caches)
+                , opaque(opaque)
+                , viewportWidth(viewportWidth)
+                , viewportHeight(viewportHeight) {
+            orthoMatrix.loadOrtho(viewportWidth, viewportHeight);
+        }
+
+        Texture* getTexture(const SkBitmap* bitmap);
+
+        void renderGlop(const BakedOpState& state, const Glop& glop);
+        RenderState& renderState;
+        Caches& caches;
+
+        bool didDraw = false;
+        bool opaque;
+
+
+        // where should these live? layer state object?
+        int viewportWidth;
+        int viewportHeight;
+        Matrix4 orthoMatrix;
+    };
+
+    static void startFrame(Info& info);
+    static void endFrame(Info& info);
+
+    /**
+     * Declare all "onBitmapOp(...)" style function for every op type.
+     *
+     * These functions will perform the actual rendering of the individual operations in OpenGL,
+     * given the transform/clip and other state built into the BakedOpState object passed in.
+     */
+    #define BAKED_OP_RENDERER_METHOD(Type) static void on##Type(Info* info, const Type& op, const BakedOpState& state);
+    MAP_OPS(BAKED_OP_RENDERER_METHOD);
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_BAKED_OP_RENDERER_H
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
new file mode 100644
index 0000000..e2201ca
--- /dev/null
+++ b/libs/hwui/BakedOpState.h
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_BAKED_OP_STATE_H
+#define ANDROID_HWUI_BAKED_OP_STATE_H
+
+#include "Matrix.h"
+#include "RecordedOp.h"
+#include "Rect.h"
+#include "Snapshot.h"
+
+namespace android {
+namespace uirenderer {
+
+namespace OpClipSideFlags {
+    enum {
+        None = 0x0,
+        Left = 0x1,
+        Top = 0x2,
+        Right = 0x4,
+        Bottom = 0x8,
+        Full = 0xF,
+        // ConservativeFull = 0x1F  needed?
+    };
+}
+
+/**
+ * Holds the resolved clip, transform, and bounds of a recordedOp, when replayed with a snapshot
+ */
+class ResolvedRenderState {
+public:
+    // TODO: remove the mapRects/matrix multiply when snapshot & recorded transforms are translates
+    ResolvedRenderState(const Snapshot& snapshot, const RecordedOp& recordedOp) {
+        /* TODO: benchmark a fast path for translate-only matrices, such as:
+        if (CC_LIKELY(snapshot.transform->getType() == Matrix4::kTypeTranslate
+                && recordedOp.localMatrix.getType() == Matrix4::kTypeTranslate)) {
+            float translateX = snapshot.transform->getTranslateX() + recordedOp.localMatrix.getTranslateX();
+            float translateY = snapshot.transform->getTranslateY() + recordedOp.localMatrix.getTranslateY();
+            transform.loadTranslate(translateX, translateY, 0);
+
+            // resolvedClipRect = intersect(parentMatrix * localClip, parentClip)
+            clipRect = recordedOp.localClipRect;
+            clipRect.translate(translateX, translateY);
+            clipRect.doIntersect(snapshot.getClipRect());
+            clipRect.snapToPixelBoundaries();
+
+            // resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect)
+            clippedBounds = recordedOp.unmappedBounds;
+            clippedBounds.translate(translateX, translateY);
+        } ... */
+
+        // resolvedMatrix = parentMatrix * localMatrix
+        transform.loadMultiply(*snapshot.transform, recordedOp.localMatrix);
+
+        // resolvedClipRect = intersect(parentMatrix * localClip, parentClip)
+        clipRect = recordedOp.localClipRect;
+        snapshot.transform->mapRect(clipRect);
+        clipRect.doIntersect(snapshot.getClipRect());
+        clipRect.snapToPixelBoundaries();
+
+        // resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect)
+        clippedBounds = recordedOp.unmappedBounds;
+        transform.mapRect(clippedBounds);
+
+        if (clipRect.left > clippedBounds.left) clipSideFlags |= OpClipSideFlags::Left;
+        if (clipRect.top > clippedBounds.top) clipSideFlags |= OpClipSideFlags::Top;
+        if (clipRect.right < clippedBounds.right) clipSideFlags |= OpClipSideFlags::Right;
+        if (clipRect.bottom < clippedBounds.bottom) clipSideFlags |= OpClipSideFlags::Bottom;
+        clippedBounds.doIntersect(clipRect);
+
+        /**
+         * TODO: once we support complex clips, we may want to reject to avoid that work where
+         * possible. Should we:
+         * 1 - quickreject based on clippedBounds, quick early (duplicating logic in resolvedOp)
+         * 2 - merge stuff into tryConstruct factory method, so it can handle quickRejection
+         *         and early return null in one place.
+         */
+    }
+    Matrix4 transform;
+    Rect clipRect;
+    int clipSideFlags = 0;
+    Rect clippedBounds;
+};
+
+/**
+ * Self-contained op wrapper, containing all resolved state required to draw the op.
+ *
+ * Stashed pointers within all point to longer lived objects, with no ownership implied.
+ */
+class BakedOpState {
+public:
+    static BakedOpState* tryConstruct(LinearAllocator& allocator,
+            const Snapshot& snapshot, const RecordedOp& recordedOp) {
+        BakedOpState* bakedOp = new (allocator) BakedOpState(
+                snapshot, recordedOp);
+        if (bakedOp->computedState.clippedBounds.isEmpty()) {
+            // bounds are empty, so op is rejected
+            allocator.rewindIfLastAlloc(bakedOp);
+            return nullptr;
+        }
+        return bakedOp;
+    }
+
+    static void* operator new(size_t size, LinearAllocator& allocator) {
+        return allocator.alloc(size);
+    }
+
+    // computed state:
+    const ResolvedRenderState computedState;
+
+    // simple state (straight pointer/value storage):
+    const float alpha;
+    const RoundRectClipState* roundRectClipState;
+    const ProjectionPathMask* projectionPathMask;
+    const RecordedOp* op;
+
+private:
+    BakedOpState(const Snapshot& snapshot, const RecordedOp& recordedOp)
+            : computedState(snapshot, recordedOp)
+            , alpha(snapshot.alpha)
+            , roundRectClipState(snapshot.roundRectClipState)
+            , projectionPathMask(snapshot.projectionPathMask)
+            , op(&recordedOp) {}
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_BAKED_OP_STATE_H
diff --git a/libs/hwui/DeferredDisplayList.cpp b/libs/hwui/DeferredDisplayList.cpp
index 0c29a9e..a1825c5 100644
--- a/libs/hwui/DeferredDisplayList.cpp
+++ b/libs/hwui/DeferredDisplayList.cpp
@@ -528,7 +528,7 @@
     int insertBatchIndex = mBatches.size();
     if (!mBatches.empty()) {
         if (state->mBounds.isEmpty()) {
-            // don't know the bounds for op, so add to last batch and start from scratch on next op
+            // don't know the bounds for op, so create new batch and start from scratch on next op
             DrawBatch* b = new DrawBatch(deferInfo);
             b->add(op, state, deferInfo.opaqueOverBounds);
             mBatches.push_back(b);
diff --git a/libs/hwui/DeferredDisplayList.h b/libs/hwui/DeferredDisplayList.h
index 7873fbd..2d5979f 100644
--- a/libs/hwui/DeferredDisplayList.h
+++ b/libs/hwui/DeferredDisplayList.h
@@ -49,7 +49,7 @@
 
 class DeferredDisplayState {
 public:
-    /** static void* operator new(size_t size); PURPOSELY OMITTED **/
+    static void* operator new(size_t size) = delete;
     static void* operator new(size_t size, LinearAllocator& allocator) {
         return allocator.alloc(size);
     }
@@ -61,7 +61,6 @@
     bool mClipValid;
     Rect mClip;
     int mClipSideFlags; // specifies which sides of the bounds are clipped, unclipped if cleared
-    bool mClipped;
     mat4 mMatrix;
     float mAlpha;
     const RoundRectClipState* mRoundRectClipState;
diff --git a/libs/hwui/DisplayList.cpp b/libs/hwui/DisplayList.cpp
index ee51da2..2337299 100644
--- a/libs/hwui/DisplayList.cpp
+++ b/libs/hwui/DisplayList.cpp
@@ -21,7 +21,13 @@
 
 #include "Debug.h"
 #include "DisplayList.h"
+#include "RenderNode.h"
+
+#if HWUI_NEW_OPS
+#include "RecordedOp.h"
+#else
 #include "DisplayListOp.h"
+#endif
 
 namespace android {
 namespace uirenderer {
@@ -61,8 +67,13 @@
     regions.clear();
 }
 
+#if HWUI_NEW_OPS
+size_t DisplayListData::addChild(RenderNodeOp* op) {
+    mReferenceHolders.push_back(op->renderNode);
+#else
 size_t DisplayListData::addChild(DrawRenderNodeOp* op) {
-    mReferenceHolders.push_back(op->renderNode());
+    mReferenceHolders.push_back(op->renderNode);
+#endif
     size_t index = mChildren.size();
     mChildren.push_back(op);
     return index;
diff --git a/libs/hwui/DisplayList.h b/libs/hwui/DisplayList.h
index 0bdb816..8ba9ac3 100644
--- a/libs/hwui/DisplayList.h
+++ b/libs/hwui/DisplayList.h
@@ -55,11 +55,12 @@
 class Rect;
 class Layer;
 
-class ClipRectOp;
-class SaveLayerOp;
-class SaveOp;
-class RestoreToCountOp;
+#if HWUI_NEW_OPS
+struct RecordedOp;
+struct RenderNodeOp;
+#else
 class DrawRenderNodeOp;
+#endif
 
 /**
  * Holds data used in the playback a tree of DisplayLists.
@@ -107,6 +108,7 @@
  */
 class DisplayListData {
     friend class DisplayListCanvas;
+    friend class RecordingCanvas;
 public:
     struct Chunk {
         // range of included ops in DLD::displayListOps
@@ -139,11 +141,21 @@
     Vector<Functor*> functors;
 
     const std::vector<Chunk>& getChunks() const {
-        return chunks;
+            return chunks;
     }
+#if HWUI_NEW_OPS
+    const std::vector<RecordedOp*>& getOps() const {
+        return ops;
+    }
+#endif
 
+#if HWUI_NEW_OPS
+    size_t addChild(RenderNodeOp* childOp);
+    const std::vector<RenderNodeOp*>& children() { return mChildren; }
+#else
     size_t addChild(DrawRenderNodeOp* childOp);
     const std::vector<DrawRenderNodeOp*>& children() { return mChildren; }
+#endif
 
     void ref(VirtualLightRefBase* prop) {
         mReferenceHolders.push_back(prop);
@@ -157,10 +169,18 @@
     }
 
 private:
+#if HWUI_NEW_OPS
+    std::vector<RecordedOp*> ops;
+#endif
+
     std::vector< sp<VirtualLightRefBase> > mReferenceHolders;
 
+#if HWUI_NEW_OPS
+    std::vector<RenderNodeOp*> mChildren;
+#else
     // list of children display lists for quick, non-drawing traversal
     std::vector<DrawRenderNodeOp*> mChildren;
+#endif
 
     std::vector<Chunk> chunks;
 
diff --git a/libs/hwui/DisplayListCanvas.cpp b/libs/hwui/DisplayListCanvas.cpp
index 77bde86..8d0ab03 100644
--- a/libs/hwui/DisplayListCanvas.cpp
+++ b/libs/hwui/DisplayListCanvas.cpp
@@ -558,16 +558,18 @@
 
 size_t DisplayListCanvas::addRenderNodeOp(DrawRenderNodeOp* op) {
     int opIndex = addDrawOp(op);
+#if !HWUI_NEW_OPS
     int childIndex = mDisplayListData->addChild(op);
 
     // update the chunk's child indices
     DisplayListData::Chunk& chunk = mDisplayListData->chunks.back();
     chunk.endChildIndex = childIndex + 1;
 
-    if (op->renderNode()->stagingProperties().isProjectionReceiver()) {
+    if (op->renderNode->stagingProperties().isProjectionReceiver()) {
         // use staging property, since recording on UI thread
         mDisplayListData->projectionReceiveIndex = opIndex;
     }
+#endif
     return opIndex;
 }
 
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index ddfc533..1f6282d 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -1397,25 +1397,26 @@
 class DrawRenderNodeOp : public DrawBoundedOp {
     friend class RenderNode; // grant RenderNode access to info of child
     friend class DisplayListData; // grant DisplayListData access to info of child
+    friend class DisplayListCanvas;
 public:
     DrawRenderNodeOp(RenderNode* renderNode, const mat4& transformFromParent, bool clipIsSimple)
             : DrawBoundedOp(0, 0, renderNode->getWidth(), renderNode->getHeight(), nullptr)
-            , mRenderNode(renderNode)
+            , renderNode(renderNode)
             , mRecordedWithPotentialStencilClip(!clipIsSimple || !transformFromParent.isSimple())
             , mTransformFromParent(transformFromParent)
             , mSkipInOrderDraw(false) {}
 
     virtual void defer(DeferStateStruct& deferStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (mRenderNode->isRenderable() && !mSkipInOrderDraw) {
-            mRenderNode->defer(deferStruct, level + 1);
+        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+            renderNode->defer(deferStruct, level + 1);
         }
     }
 
     virtual void replay(ReplayStateStruct& replayStruct, int saveCount, int level,
             bool useQuickReject) override {
-        if (mRenderNode->isRenderable() && !mSkipInOrderDraw) {
-            mRenderNode->replay(replayStruct, level + 1);
+        if (renderNode->isRenderable() && !mSkipInOrderDraw) {
+            renderNode->replay(replayStruct, level + 1);
         }
     }
 
@@ -1424,18 +1425,16 @@
     }
 
     virtual void output(int level, uint32_t logFlags) const override {
-        OP_LOG("Draw RenderNode %p %s", mRenderNode, mRenderNode->getName());
-        if (mRenderNode && (logFlags & kOpLogFlag_Recurse)) {
-            mRenderNode->output(level + 1);
+        OP_LOG("Draw RenderNode %p %s", renderNode, renderNode->getName());
+        if (renderNode && (logFlags & kOpLogFlag_Recurse)) {
+            renderNode->output(level + 1);
         }
     }
 
     virtual const char* name() override { return "DrawRenderNode"; }
 
-    RenderNode* renderNode() { return mRenderNode; }
-
 private:
-    RenderNode* mRenderNode;
+    RenderNode* renderNode;
 
     /**
      * This RenderNode was drawn into a DisplayList with the canvas in a state that will likely
diff --git a/libs/hwui/GlopBuilder.cpp b/libs/hwui/GlopBuilder.cpp
index fa166ae..d2da851 100644
--- a/libs/hwui/GlopBuilder.cpp
+++ b/libs/hwui/GlopBuilder.cpp
@@ -461,12 +461,12 @@
 // Transform
 ////////////////////////////////////////////////////////////////////////////////
 
-void GlopBuilder::setTransform(const Matrix4& canvas,
-        const int transformFlags) {
+GlopBuilder& GlopBuilder::setTransform(const Matrix4& canvas, const int transformFlags) {
     TRIGGER_STAGE(kTransformStage);
 
     mOutGlop->transform.canvas = canvas;
     mOutGlop->transform.transformFlags = transformFlags;
+    return *this;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -632,5 +632,42 @@
     mOutGlop->transform.meshTransform().mapRect(mOutGlop->bounds);
 }
 
+void GlopBuilder::dump(const Glop& glop) {
+    ALOGD("Glop Mesh");
+    const Glop::Mesh& mesh = glop.mesh;
+    ALOGD("    primitive mode: %d", mesh.primitiveMode);
+    ALOGD("    indices: buffer obj %x, indices %p", mesh.indices.bufferObject, mesh.indices.indices);
+
+    const Glop::Mesh::Vertices& vertices = glop.mesh.vertices;
+    ALOGD("    vertices: buffer obj %x, flags %x, pos %p, tex %p, clr %p, stride %d",
+            vertices.bufferObject, vertices.attribFlags,
+            vertices.position, vertices.texCoord, vertices.color, vertices.stride);
+    ALOGD("    element count: %d", mesh.elementCount);
+
+    ALOGD("Glop Fill");
+    const Glop::Fill& fill = glop.fill;
+    ALOGD("    program %p", fill.program);
+    if (fill.texture.texture) {
+        ALOGD("    texture %p, target %d, filter %d, clamp %d",
+                fill.texture.texture, fill.texture.target, fill.texture.filter, fill.texture.clamp);
+        if (fill.texture.textureTransform) {
+            fill.texture.textureTransform->dump("texture transform");
+        }
+    }
+    ALOGD_IF(fill.colorEnabled, "    color (argb) %.2f %.2f %.2f %.2f",
+            fill.color.a, fill.color.r, fill.color.g, fill.color.b);
+    ALOGD_IF(fill.filterMode != ProgramDescription::ColorFilterMode::None,
+            "    filterMode %d", (int)fill.filterMode);
+    ALOGD_IF(fill.skiaShaderData.skiaShaderType, "    shader type %d",
+            fill.skiaShaderData.skiaShaderType);
+
+    ALOGD("Glop transform");
+    glop.transform.modelView.dump("model view");
+    glop.transform.canvas.dump("canvas");
+
+    ALOGD("Glop blend %d %d", glop.blend.src, glop.blend.dst);
+    ALOGD("Glop bounds " RECT_STRING, RECT_ARGS(glop.bounds));
+}
+
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/GlopBuilder.h b/libs/hwui/GlopBuilder.h
index 8d05570..6f5802e 100644
--- a/libs/hwui/GlopBuilder.h
+++ b/libs/hwui/GlopBuilder.h
@@ -71,9 +71,9 @@
     GlopBuilder& setFillTextureLayer(Layer& layer, float alpha);
 
     GlopBuilder& setTransform(const Snapshot& snapshot, const int transformFlags) {
-        setTransform(*snapshot.transform, transformFlags);
-        return *this;
+        return setTransform(*snapshot.transform, transformFlags);
     }
+    GlopBuilder& setTransform(const Matrix4& canvas, const int transformFlags);
 
     GlopBuilder& setModelViewMapUnitToRect(const Rect destination);
     GlopBuilder& setModelViewMapUnitToRectSnap(const Rect destination);
@@ -98,11 +98,12 @@
     GlopBuilder& setRoundRectClipState(const RoundRectClipState* roundRectClipState);
 
     void build();
+
+    static void dump(const Glop& glop);
 private:
     void setFill(int color, float alphaScale,
             SkXfermode::Mode mode, Blend::ModeOrderSwap modeUsage,
             const SkShader* shader, const SkColorFilter* colorFilter);
-    void setTransform(const Matrix4& canvas, const int transformFlags);
 
     enum StageFlags {
         kInitialStage = 0,
diff --git a/libs/hwui/Matrix.h b/libs/hwui/Matrix.h
index ed517ac..c017638 100644
--- a/libs/hwui/Matrix.h
+++ b/libs/hwui/Matrix.h
@@ -126,6 +126,9 @@
     void loadMultiply(const Matrix4& u, const Matrix4& v);
 
     void loadOrtho(float left, float right, float bottom, float top, float near, float far);
+    void loadOrtho(int width, int height) {
+        loadOrtho(0, width, height, 0, -1, 1);
+    }
 
     uint8_t getType() const;
 
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
new file mode 100644
index 0000000..1d8be2b
--- /dev/null
+++ b/libs/hwui/OpReorderer.cpp
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "OpReorderer.h"
+
+#include "utils/PaintUtils.h"
+#include "RenderNode.h"
+
+#include "SkCanvas.h"
+#include "utils/Trace.h"
+
+namespace android {
+namespace uirenderer {
+
+class BatchBase {
+
+public:
+    BatchBase(batchid_t batchId, BakedOpState* op, bool merging)
+        : mBatchId(batchId)
+        , mMerging(merging) {
+        mBounds = op->computedState.clippedBounds;
+        mOps.push_back(op);
+    }
+
+    bool intersects(const Rect& rect) const {
+        if (!rect.intersects(mBounds)) return false;
+
+        for (const BakedOpState* op : mOps) {
+            if (rect.intersects(op->computedState.clippedBounds)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    batchid_t getBatchId() const { return mBatchId; }
+    bool isMerging() const { return mMerging; }
+
+    const std::vector<BakedOpState*>& getOps() const { return mOps; }
+
+    void dump() const {
+        ALOGD("    Batch %p, merging %d, bounds " RECT_STRING, this, mMerging, RECT_ARGS(mBounds));
+    }
+protected:
+    batchid_t mBatchId;
+    Rect mBounds;
+    std::vector<BakedOpState*> mOps;
+    bool mMerging;
+};
+
+class OpBatch : public BatchBase {
+public:
+    static void* operator new(size_t size, LinearAllocator& allocator) {
+        return allocator.alloc(size);
+    }
+
+    OpBatch(batchid_t batchId, BakedOpState* op)
+            : BatchBase(batchId, op, false) {
+    }
+
+    void batchOp(BakedOpState* op) {
+        mBounds.unionWith(op->computedState.clippedBounds);
+        mOps.push_back(op);
+    }
+};
+
+class MergingOpBatch : public BatchBase {
+public:
+    static void* operator new(size_t size, LinearAllocator& allocator) {
+        return allocator.alloc(size);
+    }
+
+    MergingOpBatch(batchid_t batchId, BakedOpState* op)
+            : BatchBase(batchId, op, true) {
+    }
+
+    /*
+     * Helper for determining if a new op can merge with a MergingDrawBatch based on their bounds
+     * and clip side flags. Positive bounds delta means new bounds fit in old.
+     */
+    static inline bool checkSide(const int currentFlags, const int newFlags, const int side,
+            float boundsDelta) {
+        bool currentClipExists = currentFlags & side;
+        bool newClipExists = newFlags & side;
+
+        // if current is clipped, we must be able to fit new bounds in current
+        if (boundsDelta > 0 && currentClipExists) return false;
+
+        // if new is clipped, we must be able to fit current bounds in new
+        if (boundsDelta < 0 && newClipExists) return false;
+
+        return true;
+    }
+
+    static bool paintIsDefault(const SkPaint& paint) {
+        return paint.getAlpha() == 255
+                && paint.getColorFilter() == nullptr
+                && paint.getShader() == nullptr;
+    }
+
+    static bool paintsAreEquivalent(const SkPaint& a, const SkPaint& b) {
+        return a.getAlpha() == b.getAlpha()
+                && a.getColorFilter() == b.getColorFilter()
+                && a.getShader() == b.getShader();
+    }
+
+    /*
+     * Checks if a (mergeable) op can be merged into this batch
+     *
+     * If true, the op's multiDraw must be guaranteed to handle both ops simultaneously, so it is
+     * important to consider all paint attributes used in the draw calls in deciding both a) if an
+     * op tries to merge at all, and b) if the op can merge with another set of ops
+     *
+     * False positives can lead to information from the paints of subsequent merged operations being
+     * dropped, so we make simplifying qualifications on the ops that can merge, per op type.
+     */
+    bool canMergeWith(BakedOpState* op) const {
+        bool isTextBatch = getBatchId() == OpBatchType::Text
+                || getBatchId() == OpBatchType::ColorText;
+
+        // Overlapping other operations is only allowed for text without shadow. For other ops,
+        // multiDraw isn't guaranteed to overdraw correctly
+        if (!isTextBatch || PaintUtils::hasTextShadow(op->op->paint)) {
+            if (intersects(op->computedState.clippedBounds)) return false;
+        }
+
+        const BakedOpState* lhs = op;
+        const BakedOpState* rhs = mOps[0];
+
+        if (!MathUtils::areEqual(lhs->alpha, rhs->alpha)) return false;
+
+        // Identical round rect clip state means both ops will clip in the same way, or not at all.
+        // As the state objects are const, we can compare their pointers to determine mergeability
+        if (lhs->roundRectClipState != rhs->roundRectClipState) return false;
+        if (lhs->projectionPathMask != rhs->projectionPathMask) return false;
+
+        /* Clipping compatibility check
+         *
+         * Exploits the fact that if a op or batch is clipped on a side, its bounds will equal its
+         * clip for that side.
+         */
+        const int currentFlags = mClipSideFlags;
+        const int newFlags = op->computedState.clipSideFlags;
+        if (currentFlags != OpClipSideFlags::None || newFlags != OpClipSideFlags::None) {
+            const Rect& opBounds = op->computedState.clippedBounds;
+            float boundsDelta = mBounds.left - opBounds.left;
+            if (!checkSide(currentFlags, newFlags, OpClipSideFlags::Left, boundsDelta)) return false;
+            boundsDelta = mBounds.top - opBounds.top;
+            if (!checkSide(currentFlags, newFlags, OpClipSideFlags::Top, boundsDelta)) return false;
+
+            // right and bottom delta calculation reversed to account for direction
+            boundsDelta = opBounds.right - mBounds.right;
+            if (!checkSide(currentFlags, newFlags, OpClipSideFlags::Right, boundsDelta)) return false;
+            boundsDelta = opBounds.bottom - mBounds.bottom;
+            if (!checkSide(currentFlags, newFlags, OpClipSideFlags::Bottom, boundsDelta)) return false;
+        }
+
+        const SkPaint* newPaint = op->op->paint;
+        const SkPaint* oldPaint = mOps[0]->op->paint;
+
+        if (newPaint == oldPaint) {
+            // if paints are equal, then modifiers + paint attribs don't need to be compared
+            return true;
+        } else if (newPaint && !oldPaint) {
+            return paintIsDefault(*newPaint);
+        } else if (!newPaint && oldPaint) {
+            return paintIsDefault(*oldPaint);
+        }
+        return paintsAreEquivalent(*newPaint, *oldPaint);
+    }
+
+    void mergeOp(BakedOpState* op) {
+        mBounds.unionWith(op->computedState.clippedBounds);
+        mOps.push_back(op);
+
+        const int newClipSideFlags = op->computedState.clipSideFlags;
+        mClipSideFlags |= newClipSideFlags;
+
+        const Rect& opClip = op->computedState.clipRect;
+        if (newClipSideFlags & OpClipSideFlags::Left) mClipRect.left = opClip.left;
+        if (newClipSideFlags & OpClipSideFlags::Top) mClipRect.top = opClip.top;
+        if (newClipSideFlags & OpClipSideFlags::Right) mClipRect.right = opClip.right;
+        if (newClipSideFlags & OpClipSideFlags::Bottom) mClipRect.bottom = opClip.bottom;
+    }
+
+private:
+    int mClipSideFlags = 0;
+    Rect mClipRect;
+};
+
+class NullClient: public CanvasStateClient {
+    void onViewportInitialized() override {}
+    void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {}
+    GLuint getTargetFbo() const override { return 0; }
+};
+static NullClient sNullClient;
+
+OpReorderer::OpReorderer()
+        : mCanvasState(sNullClient) {
+}
+
+void OpReorderer::defer(int viewportWidth, int viewportHeight,
+        const std::vector< sp<RenderNode> >& nodes) {
+    mCanvasState.initializeSaveStack(viewportWidth, viewportHeight,
+            0, 0, viewportWidth, viewportHeight, Vector3());
+    for (const sp<RenderNode>& node : nodes) {
+        if (node->nothingToDraw()) continue;
+
+        // TODO: dedupe this code with onRenderNode()
+        mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
+        if (node->applyViewProperties(mCanvasState)) {
+            // not rejected do ops...
+            const DisplayListData& data = node->getDisplayListData();
+            deferImpl(data.getChunks(), data.getOps());
+        }
+        mCanvasState.restore();
+    }
+}
+
+void OpReorderer::defer(int viewportWidth, int viewportHeight,
+        const std::vector<DisplayListData::Chunk>& chunks, const std::vector<RecordedOp*>& ops) {
+    ATRACE_NAME("prepare drawing commands");
+    mCanvasState.initializeSaveStack(viewportWidth, viewportHeight,
+            0, 0, viewportWidth, viewportHeight, Vector3());
+    deferImpl(chunks, ops);
+}
+
+/**
+ * Used to define a list of lambdas referencing private OpReorderer::onXXXXOp() methods.
+ *
+ * This allows opIds embedded in the RecordedOps to be used for dispatching to these lambdas. E.g. a
+ * BitmapOp op then would be dispatched to OpReorderer::onBitmapOp(const BitmapOp&)
+ */
+#define OP_RECIEVER(Type) \
+        [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
+void OpReorderer::deferImpl(const std::vector<DisplayListData::Chunk>& chunks,
+        const std::vector<RecordedOp*>& ops) {
+    static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
+        MAP_OPS(OP_RECIEVER)
+    };
+    for (const DisplayListData::Chunk& chunk : chunks) {
+        for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
+            const RecordedOp* op = ops[opIndex];
+            receivers[op->opId](*this, *op);
+        }
+    }
+}
+
+void OpReorderer::replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers) {
+    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);
+        }
+    }
+}
+
+BakedOpState* OpReorderer::bakeOpState(const RecordedOp& recordedOp) {
+    return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp);
+}
+
+void OpReorderer::onRenderNodeOp(const RenderNodeOp& op) {
+    if (op.renderNode->nothingToDraw()) {
+        return;
+    }
+    mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag);
+
+    // apply state from RecordedOp
+    mCanvasState.concatMatrix(op.localMatrix);
+    mCanvasState.clipRect(op.localClipRect.left, op.localClipRect.top,
+            op.localClipRect.right, op.localClipRect.bottom, SkRegion::kIntersect_Op);
+
+    // apply RenderProperties state
+    if (op.renderNode->applyViewProperties(mCanvasState)) {
+        // not rejected do ops...
+        const DisplayListData& data = op.renderNode->getDisplayListData();
+        deferImpl(data.getChunks(), data.getOps());
+    }
+    mCanvasState.restore();
+}
+
+static batchid_t tessellatedBatchId(const SkPaint& paint) {
+    return paint.getPathEffect()
+            ? OpBatchType::AlphaMaskTexture
+            : (paint.isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices);
+}
+
+void OpReorderer::onBitmapOp(const BitmapOp& op) {
+    BakedOpState* bakedStateOp = bakeOpState(op);
+    if (!bakedStateOp) return; // quick rejected
+
+    mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
+    // TODO: AssetAtlas
+
+    deferMergeableOp(bakedStateOp, OpBatchType::Bitmap, mergeId);
+}
+
+void OpReorderer::onRectOp(const RectOp& op) {
+    BakedOpState* bakedStateOp = bakeOpState(op);
+    if (!bakedStateOp) return; // quick rejected
+    deferUnmergeableOp(bakedStateOp, tessellatedBatchId(*op.paint));
+}
+
+void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {
+    BakedOpState* bakedStateOp = bakeOpState(op);
+    if (!bakedStateOp) return; // quick rejected
+    deferUnmergeableOp(bakedStateOp, OpBatchType::Vertices);
+}
+
+// iterate back toward target to see if anything drawn since should overlap the new op
+// if no target, merging ops still interate to find similar batch to insert after
+void OpReorderer::locateInsertIndex(int batchId, const Rect& clippedBounds,
+        BatchBase** targetBatch, size_t* insertBatchIndex) const {
+    for (size_t i = mBatches.size() - 1; i >= mEarliestBatchIndex; i--) {
+        BatchBase* overBatch = mBatches[i];
+
+        if (overBatch == *targetBatch) break;
+
+        // TODO: also consider shader shared between batch types
+        if (batchId == overBatch->getBatchId()) {
+            *insertBatchIndex = i + 1;
+            if (!*targetBatch) break; // found insert position, quit
+        }
+
+        if (overBatch->intersects(clippedBounds)) {
+            // NOTE: it may be possible to optimize for special cases where two operations
+            // of the same batch/paint could swap order, such as with a non-mergeable
+            // (clipped) and a mergeable text operation
+            *targetBatch = nullptr;
+            break;
+        }
+    }
+}
+
+void OpReorderer::deferUnmergeableOp(BakedOpState* op, batchid_t batchId) {
+    OpBatch* targetBatch = mBatchLookup[batchId];
+
+    size_t insertBatchIndex = mBatches.size();
+    if (targetBatch) {
+        locateInsertIndex(batchId, op->computedState.clippedBounds,
+                (BatchBase**)(&targetBatch), &insertBatchIndex);
+    }
+
+    if (targetBatch) {
+        targetBatch->batchOp(op);
+    } else  {
+        // new non-merging batch
+        targetBatch = new (mAllocator) OpBatch(batchId, op);
+        mBatchLookup[batchId] = targetBatch;
+        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
+    }
+}
+
+// insertion point of a new batch, will hopefully be immediately after similar batch
+// (generally, should be similar shader)
+void OpReorderer::deferMergeableOp(BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
+    MergingOpBatch* targetBatch = nullptr;
+
+    // Try to merge with any existing batch with same mergeId
+    auto getResult = mMergingBatches[batchId].find(mergeId);
+    if (getResult != mMergingBatches[batchId].end()) {
+        targetBatch = getResult->second;
+        if (!targetBatch->canMergeWith(op)) {
+            targetBatch = nullptr;
+        }
+    }
+
+    size_t insertBatchIndex = mBatches.size();
+    locateInsertIndex(batchId, op->computedState.clippedBounds,
+            (BatchBase**)(&targetBatch), &insertBatchIndex);
+
+    if (targetBatch) {
+        targetBatch->mergeOp(op);
+    } else  {
+        // new merging batch
+        targetBatch = new (mAllocator) MergingOpBatch(batchId, op);
+        mMergingBatches[batchId].insert(std::make_pair(mergeId, targetBatch));
+
+        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
+    }
+}
+
+void OpReorderer::dump() {
+    for (const BatchBase* batch : mBatches) {
+        batch->dump();
+    }
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
new file mode 100644
index 0000000..b99172c
--- /dev/null
+++ b/libs/hwui/OpReorderer.h
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_OP_REORDERER_H
+#define ANDROID_HWUI_OP_REORDERER_H
+
+#include "BakedOpState.h"
+#include "CanvasState.h"
+#include "DisplayList.h"
+#include "RecordedOp.h"
+
+#include <vector>
+#include <unordered_map>
+
+namespace android {
+namespace uirenderer {
+
+class BakedOpState;
+class BatchBase;
+class MergingOpBatch;
+class OpBatch;
+class Rect;
+
+typedef int batchid_t;
+typedef const void* mergeid_t;
+
+namespace OpBatchType {
+    enum {
+        None = 0, // Don't batch
+        Bitmap,
+        Patch,
+        AlphaVertices,
+        Vertices,
+        AlphaMaskTexture,
+        Text,
+        ColorText,
+
+        Count // must be last
+    };
+}
+
+class OpReorderer {
+public:
+    OpReorderer();
+
+    // TODO: not final, just presented this way for simplicity. Layers too?
+    void defer(int viewportWidth, int viewportHeight, const std::vector< sp<RenderNode> >& nodes);
+
+    void defer(int viewportWidth, int viewportHeight,
+            const std::vector<DisplayListData::Chunk>& chunks, const std::vector<RecordedOp*>& ops);
+    typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpReceiver;
+
+    /**
+     * replayBakedOps() is templated based on what class will recieve ops being replayed.
+     *
+     * 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:
+     *
+     * StaticReceiver::onBitmapOp(Arg* arg, const BitmapOp& op, const BakedOpState& state);
+     */
+#define BAKED_OP_RECEIVER(Type) \
+    [](void* internalArg, const RecordedOp& op, const BakedOpState& state) { \
+        StaticReceiver::on##Type(static_cast<Arg*>(internalArg), static_cast<const Type&>(op), state); \
+    },
+    template <typename StaticReceiver, typename Arg>
+    void replayBakedOps(Arg* arg) {
+        static BakedOpReceiver receivers[] = {
+            MAP_OPS(BAKED_OP_RECEIVER)
+        };
+        StaticReceiver::startFrame(*arg);
+        replayBakedOpsImpl((void*)arg, receivers);
+        StaticReceiver::endFrame(*arg);
+    }
+private:
+    BakedOpState* bakeOpState(const RecordedOp& recordedOp);
+
+    void deferImpl(const std::vector<DisplayListData::Chunk>& chunks,
+            const std::vector<RecordedOp*>& ops);
+
+    void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers);
+
+    /**
+     * Declares all OpReorderer::onXXXXOp() methods for every RecordedOp type.
+     *
+     * These private methods are called from within deferImpl to defer each individual op
+     * type differently.
+     */
+#define INTERNAL_OP_HANDLER(Type) \
+    void on##Type(const Type& op);
+    MAP_OPS(INTERNAL_OP_HANDLER)
+
+    // iterate back toward target to see if anything drawn since should overlap the new op
+    // if no target, merging ops still iterate to find similar batch to insert after
+    void locateInsertIndex(int batchId, const Rect& clippedBounds,
+            BatchBase** targetBatch, size_t* insertBatchIndex) const;
+
+    void deferUnmergeableOp(BakedOpState* op, batchid_t batchId);
+
+    // insertion point of a new batch, will hopefully be immediately after similar batch
+    // (generally, should be similar shader)
+    void deferMergeableOp(BakedOpState* op, batchid_t batchId, mergeid_t mergeId);
+
+    void dump();
+
+    std::vector<BatchBase*> mBatches;
+
+    /**
+     * Maps the mergeid_t returned by an op's getMergeId() to the most recently seen
+     * MergingDrawBatch of that id. These ids are unique per draw type and guaranteed to not
+     * collide, which avoids the need to resolve mergeid collisions.
+     */
+    std::unordered_map<mergeid_t, MergingOpBatch*> mMergingBatches[OpBatchType::Count];
+
+    // Maps batch ids to the most recent *non-merging* batch of that id
+    OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };
+    CanvasState mCanvasState;
+
+    // contains ResolvedOps and Batches
+    LinearAllocator mAllocator;
+
+    size_t mEarliestBatchIndex = 0;
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_OP_REORDERER_H
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
new file mode 100644
index 0000000..a69f030
--- /dev/null
+++ b/libs/hwui/RecordedOp.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_RECORDED_OP_H
+#define ANDROID_HWUI_RECORDED_OP_H
+
+#include "utils/LinearAllocator.h"
+#include "Rect.h"
+#include "Matrix.h"
+
+#include "SkXfermode.h"
+
+class SkBitmap;
+class SkPaint;
+
+namespace android {
+namespace uirenderer {
+
+class RenderNode;
+struct Vertex;
+
+/**
+ * The provided macro is executed for each op type in order, with the results separated by commas.
+ *
+ * This serves as the authoritative list of ops, used for generating ID enum, and ID based LUTs.
+ */
+#define MAP_OPS(OP_FN) \
+        OP_FN(BitmapOp) \
+        OP_FN(RectOp) \
+        OP_FN(RenderNodeOp) \
+        OP_FN(SimpleRectsOp)
+
+// Generate OpId enum
+#define IDENTITY_FN(Type) Type,
+namespace RecordedOpId {
+    enum {
+        MAP_OPS(IDENTITY_FN)
+        Count,
+    };
+}
+static_assert(RecordedOpId::BitmapOp == 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
+#define BASE_PARAMS_PAINTLESS const Rect& unmappedBounds, const Matrix4& localMatrix, const Rect& localClipRect
+#define SUPER(Type) RecordedOp(RecordedOpId::Type, unmappedBounds, localMatrix, localClipRect, paint)
+#define SUPER_PAINTLESS(Type) RecordedOp(RecordedOpId::Type, unmappedBounds, localMatrix, localClipRect, nullptr)
+
+struct RecordedOp {
+    /* ID from RecordedOpId - generally used for jumping into function tables */
+    const int opId;
+
+    /* bounds in *local* space, without accounting for DisplayList transformation */
+    const Rect unmappedBounds;
+
+    /* transform in recording space (vs DisplayList origin) */
+    const Matrix4 localMatrix;
+
+    /* clip in recording space */
+    const Rect localClipRect;
+
+    /* optional paint, stored in base object to simplify merging logic */
+    const SkPaint* paint;
+protected:
+    RecordedOp(unsigned int opId, BASE_PARAMS)
+            : opId(opId)
+            , unmappedBounds(unmappedBounds)
+            , localMatrix(localMatrix)
+            , localClipRect(localClipRect)
+            , paint(paint) {}
+};
+
+struct RenderNodeOp : RecordedOp {
+    RenderNodeOp(BASE_PARAMS_PAINTLESS, RenderNode* renderNode)
+            : SUPER_PAINTLESS(RenderNodeOp)
+            , renderNode(renderNode) {}
+    RenderNode * renderNode; // not const, since drawing modifies it (somehow...)
+};
+
+struct BitmapOp : RecordedOp {
+    BitmapOp(BASE_PARAMS, const SkBitmap* bitmap)
+            : SUPER(BitmapOp)
+            , bitmap(bitmap) {}
+    const SkBitmap* bitmap;
+    // TODO: asset atlas/texture id lookup?
+};
+
+struct RectOp : RecordedOp {
+    RectOp(BASE_PARAMS)
+            : SUPER(RectOp) {}
+};
+
+struct SimpleRectsOp : RecordedOp { // Filled, no AA (TODO: better name?)
+    SimpleRectsOp(BASE_PARAMS, Vertex* vertices, size_t vertexCount)
+            : SUPER(SimpleRectsOp)
+            , vertices(vertices)
+            , vertexCount(vertexCount) {}
+    Vertex* vertices;
+    const size_t vertexCount;
+};
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_RECORDED_OP_H
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
new file mode 100644
index 0000000..c4debd6
--- /dev/null
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "RecordingCanvas.h"
+
+#include "RecordedOp.h"
+#include "RenderNode.h"
+
+namespace android {
+namespace uirenderer {
+
+RecordingCanvas::RecordingCanvas(size_t width, size_t height)
+        : mState(*this)
+        , mResourceCache(ResourceCache::getInstance()) {
+    reset(width, height);
+}
+
+RecordingCanvas::~RecordingCanvas() {
+    LOG_ALWAYS_FATAL_IF(mDisplayListData,
+            "Destroyed a RecordingCanvas during a record!");
+}
+
+void RecordingCanvas::reset(int width, int height) {
+    LOG_ALWAYS_FATAL_IF(mDisplayListData,
+            "prepareDirty called a second time during a recording!");
+    mDisplayListData = new DisplayListData();
+
+    mState.initializeSaveStack(width, height, 0, 0, width, height, Vector3());
+
+    mDeferredBarrierType = kBarrier_InOrder;
+    mState.setDirtyClip(false);
+    mRestoreSaveCount = -1;
+}
+
+DisplayListData* RecordingCanvas::finishRecording() {
+    mPaintMap.clear();
+    mRegionMap.clear();
+    mPathMap.clear();
+    DisplayListData* data = mDisplayListData;
+    mDisplayListData = nullptr;
+    mSkiaCanvasProxy.reset(nullptr);
+    return data;
+}
+
+SkCanvas* RecordingCanvas::asSkCanvas() {
+    LOG_ALWAYS_FATAL_IF(!mDisplayListData,
+            "attempting to get an SkCanvas when we are not recording!");
+    if (!mSkiaCanvasProxy) {
+        mSkiaCanvasProxy.reset(new SkiaCanvasProxy(this));
+    }
+
+    // SkCanvas instances default to identity transform, but should inherit
+    // the state of this Canvas; if this code was in the SkiaCanvasProxy
+    // constructor, we couldn't cache mSkiaCanvasProxy.
+    SkMatrix parentTransform;
+    getMatrix(&parentTransform);
+    mSkiaCanvasProxy.get()->setMatrix(parentTransform);
+
+    return mSkiaCanvasProxy.get();
+}
+
+// ----------------------------------------------------------------------------
+// android/graphics/Canvas state operations
+// ----------------------------------------------------------------------------
+// Save (layer)
+int RecordingCanvas::save(SkCanvas::SaveFlags flags) {
+    return mState.save((int) flags);
+}
+
+void RecordingCanvas::RecordingCanvas::restore() {
+    if (mRestoreSaveCount < 0) {
+        restoreToCount(getSaveCount() - 1);
+        return;
+    }
+
+    mRestoreSaveCount--;
+    mState.restore();
+}
+
+void RecordingCanvas::restoreToCount(int saveCount) {
+    mRestoreSaveCount = saveCount;
+    mState.restoreToCount(saveCount);
+}
+
+int RecordingCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint,
+        SkCanvas::SaveFlags flags) {
+    LOG_ALWAYS_FATAL("TODO");
+    return 0;
+}
+
+// Matrix
+void RecordingCanvas::rotate(float degrees) {
+    if (degrees == 0) return;
+
+    mState.rotate(degrees);
+}
+
+void RecordingCanvas::scale(float sx, float sy) {
+    if (sx == 1 && sy == 1) return;
+
+    mState.scale(sx, sy);
+}
+
+void RecordingCanvas::skew(float sx, float sy) {
+    mState.skew(sx, sy);
+}
+
+void RecordingCanvas::translate(float dx, float dy) {
+    if (dx == 0 && dy == 0) return;
+
+    mState.translate(dx, dy, 0);
+}
+
+// Clip
+bool RecordingCanvas::getClipBounds(SkRect* outRect) const {
+    Rect bounds = mState.getLocalClipBounds();
+    *outRect = SkRect::MakeLTRB(bounds.left, bounds.top, bounds.right, bounds.bottom);
+    return !(outRect->isEmpty());
+}
+bool RecordingCanvas::quickRejectRect(float left, float top, float right, float bottom) const {
+    return mState.quickRejectConservative(left, top, right, bottom);
+}
+bool RecordingCanvas::quickRejectPath(const SkPath& path) const {
+    SkRect bounds = path.getBounds();
+    return mState.quickRejectConservative(bounds.fLeft, bounds.fTop, bounds.fRight, bounds.fBottom);
+}
+bool RecordingCanvas::clipRect(float left, float top, float right, float bottom, SkRegion::Op op) {
+    return mState.clipRect(left, top, right, bottom, op);
+}
+bool RecordingCanvas::clipPath(const SkPath* path, SkRegion::Op op) {
+    return mState.clipPath(path, op);
+}
+bool RecordingCanvas::clipRegion(const SkRegion* region, SkRegion::Op op) {
+    return mState.clipRegion(region, op);
+}
+
+// ----------------------------------------------------------------------------
+// android/graphics/Canvas draw operations
+// ----------------------------------------------------------------------------
+void RecordingCanvas::drawColor(int color, SkXfermode::Mode mode) {
+    SkPaint paint;
+    paint.setColor(color);
+    paint.setXfermodeMode(mode);
+    drawPaint(paint);
+}
+
+void RecordingCanvas::drawPaint(const SkPaint& paint) {
+    // TODO: more efficient recording?
+    Matrix4 identity;
+    identity.loadIdentity();
+
+    addOp(new (alloc()) RectOp(
+            mState.getRenderTargetClipBounds(),
+            identity,
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint)));
+}
+
+// Geometry
+void RecordingCanvas::drawPoints(const float* points, int count, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawLines(const float* points, int count, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawRect(float left, float top, float right, float bottom, const SkPaint& paint) {
+    addOp(new (alloc()) RectOp(
+            Rect(left, top, right, bottom),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint)));
+}
+
+void RecordingCanvas::drawSimpleRects(const float* rects, int vertexCount, const SkPaint* paint) {
+    if (rects == nullptr) return;
+
+    Vertex* rectData = (Vertex*) mDisplayListData->allocator.alloc(vertexCount * sizeof(Vertex));
+    Vertex* vertex = rectData;
+
+    float left = FLT_MAX;
+    float top = FLT_MAX;
+    float right = FLT_MIN;
+    float bottom = FLT_MIN;
+    for (int index = 0; index < vertexCount; index += 4) {
+        float l = rects[index + 0];
+        float t = rects[index + 1];
+        float r = rects[index + 2];
+        float b = rects[index + 3];
+
+        Vertex::set(vertex++, l, t);
+        Vertex::set(vertex++, r, t);
+        Vertex::set(vertex++, l, b);
+        Vertex::set(vertex++, r, b);
+
+        left = std::min(left, l);
+        top = std::min(top, t);
+        right = std::max(right, r);
+        bottom = std::max(bottom, b);
+    }
+    addOp(new (alloc()) SimpleRectsOp(
+            Rect(left, top, right, bottom),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(paint), rectData, vertexCount));
+}
+
+void RecordingCanvas::drawRegion(const SkRegion& region, const SkPaint& paint) {
+    if (paint.getStyle() == SkPaint::kFill_Style
+            && (!paint.isAntiAlias() || mState.currentTransform()->isSimple())) {
+        int count = 0;
+        Vector<float> rects;
+        SkRegion::Iterator it(region);
+        while (!it.done()) {
+            const SkIRect& r = it.rect();
+            rects.push(r.fLeft);
+            rects.push(r.fTop);
+            rects.push(r.fRight);
+            rects.push(r.fBottom);
+            count += 4;
+            it.next();
+        }
+        drawSimpleRects(rects.array(), count, &paint);
+    } else {
+        SkRegion::Iterator it(region);
+        while (!it.done()) {
+            const SkIRect& r = it.rect();
+            drawRect(r.fLeft, r.fTop, r.fRight, r.fBottom, paint);
+            it.next();
+        }
+    }
+}
+void RecordingCanvas::drawRoundRect(float left, float top, float right, float bottom,
+            float rx, float ry, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawArc(float left, float top, float right, float bottom,
+            float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+
+// Bitmap-based
+void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, float left, float top, const SkPaint* paint) {
+    save(SkCanvas::kMatrix_SaveFlag);
+    translate(left, top);
+    drawBitmap(&bitmap, paint);
+    restore();
+}
+
+void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, const SkMatrix& matrix,
+                            const SkPaint* paint) {
+    if (matrix.isIdentity()) {
+        drawBitmap(&bitmap, paint);
+    } else if (!(matrix.getType() & ~(SkMatrix::kScale_Mask | SkMatrix::kTranslate_Mask))
+            && MathUtils::isPositive(matrix.getScaleX())
+            && MathUtils::isPositive(matrix.getScaleY())) {
+        // SkMatrix::isScaleTranslate() not available in L
+        SkRect src;
+        SkRect dst;
+        bitmap.getBounds(&src);
+        matrix.mapRect(&dst, src);
+        drawBitmap(bitmap, src.fLeft, src.fTop, src.fRight, src.fBottom,
+                   dst.fLeft, dst.fTop, dst.fRight, dst.fBottom, paint);
+    } else {
+        save(SkCanvas::kMatrix_SaveFlag);
+        concat(matrix);
+        drawBitmap(&bitmap, paint);
+        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) {
+    if (srcLeft == 0 && srcTop == 0
+            && srcRight == bitmap.width()
+            && srcBottom == bitmap.height()
+            && (srcBottom - srcTop == dstBottom - dstTop)
+            && (srcRight - srcLeft == dstRight - dstLeft)) {
+        // transform simple rect to rect drawing case into position bitmap ops, since they merge
+        save(SkCanvas::kMatrix_SaveFlag);
+        translate(dstLeft, dstTop);
+        drawBitmap(&bitmap, paint);
+        restore();
+    } else {
+        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) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+
+// Text
+void RecordingCanvas::drawText(const uint16_t* glyphs, const float* positions, int count,
+            const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop,
+            float boundsRight, float boundsBottom, float totalAdvance) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawPosText(const uint16_t* text, const float* positions, int count,
+            int posCount, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+void RecordingCanvas::drawTextOnPath(const uint16_t* glyphs, int count, const SkPath& path,
+            float hOffset, float vOffset, const SkPaint& paint) {
+    LOG_ALWAYS_FATAL("TODO!");
+}
+
+void RecordingCanvas::drawBitmap(const SkBitmap* bitmap, const SkPaint* paint) {
+    addOp(new (alloc()) BitmapOp(
+            Rect(0, 0, bitmap->width(), bitmap->height()),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(paint), refBitmap(*bitmap)));
+}
+void RecordingCanvas::drawRenderNode(RenderNode* renderNode) {
+    RenderNodeOp* op = new (alloc()) RenderNodeOp(
+            Rect(0, 0, renderNode->getWidth(), renderNode->getHeight()), // are these safe? they're theoretically dynamic
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            renderNode);
+    int opIndex = addOp(op);
+    int childIndex = mDisplayListData->addChild(op);
+
+    // update the chunk's child indices
+    DisplayListData::Chunk& chunk = mDisplayListData->chunks.back();
+    chunk.endChildIndex = childIndex + 1;
+
+    if (renderNode->stagingProperties().isProjectionReceiver()) {
+        // use staging property, since recording on UI thread
+        mDisplayListData->projectionReceiveIndex = opIndex;
+    }
+}
+
+size_t RecordingCanvas::addOp(RecordedOp* op) {
+    // TODO: validate if "addDrawOp" quickrejection logic is useful before adding
+    int insertIndex = mDisplayListData->ops.size();
+    mDisplayListData->ops.push_back(op);
+    if (mDeferredBarrierType != kBarrier_None) {
+        // op is first in new chunk
+        mDisplayListData->chunks.emplace_back();
+        DisplayListData::Chunk& newChunk = mDisplayListData->chunks.back();
+        newChunk.beginOpIndex = insertIndex;
+        newChunk.endOpIndex = insertIndex + 1;
+        newChunk.reorderChildren = (mDeferredBarrierType == kBarrier_OutOfOrder);
+
+        int nextChildIndex = mDisplayListData->children().size();
+        newChunk.beginChildIndex = newChunk.endChildIndex = nextChildIndex;
+        mDeferredBarrierType = kBarrier_None;
+    } else {
+        // standard case - append to existing chunk
+        mDisplayListData->chunks.back().endOpIndex = insertIndex + 1;
+    }
+    return insertIndex;
+}
+
+void RecordingCanvas::refBitmapsInShader(const SkShader* shader) {
+    if (!shader) return;
+
+    // If this paint has an SkShader that has an SkBitmap add
+    // it to the bitmap pile
+    SkBitmap bitmap;
+    SkShader::TileMode xy[2];
+    if (shader->asABitmap(&bitmap, nullptr, xy) == SkShader::kDefault_BitmapType) {
+        refBitmap(bitmap);
+        return;
+    }
+    SkShader::ComposeRec rec;
+    if (shader->asACompose(&rec)) {
+        refBitmapsInShader(rec.fShaderA);
+        refBitmapsInShader(rec.fShaderB);
+        return;
+    }
+}
+
+}; // namespace uirenderer
+}; // namespace android
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
new file mode 100644
index 0000000..ed299e5
--- /dev/null
+++ b/libs/hwui/RecordingCanvas.h
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_HWUI_RECORDING_CANVAS_H
+#define ANDROID_HWUI_RECORDING_CANVAS_H
+
+#include "Canvas.h"
+#include "CanvasState.h"
+#include "DisplayList.h"
+#include "utils/LinearAllocator.h"
+#include "utils/NinePatch.h"
+#include "ResourceCache.h"
+#include "SkiaCanvasProxy.h"
+#include "Snapshot.h"
+
+#include "SkDrawFilter.h"
+#include <vector>
+
+namespace android {
+namespace uirenderer {
+
+class OpReceiver;
+struct RecordedOp;
+
+class RecordingCanvas: public Canvas, public CanvasStateClient {
+public:
+    RecordingCanvas(size_t width, size_t height);
+    virtual ~RecordingCanvas();
+
+    void reset(int width, int height);
+    DisplayListData* finishRecording();
+
+// ----------------------------------------------------------------------------
+// MISC HWUI OPERATIONS - TODO: CATEGORIZE
+// ----------------------------------------------------------------------------
+    void insertReorderBarrier(bool enableReorder) {}
+    void drawRenderNode(RenderNode* renderNode);
+
+// ----------------------------------------------------------------------------
+// CanvasStateClient interface
+// ----------------------------------------------------------------------------
+    virtual void onViewportInitialized() override {}
+    virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override {}
+    virtual GLuint getTargetFbo() const override { return -1; }
+
+// ----------------------------------------------------------------------------
+// android/graphics/Canvas interface
+// ----------------------------------------------------------------------------
+    virtual SkCanvas* asSkCanvas() override;
+
+    virtual void setBitmap(const SkBitmap& bitmap) override {
+        LOG_ALWAYS_FATAL("RecordingCanvas is not backed by a bitmap.");
+    }
+
+    virtual bool isOpaque() override { return false; }
+    virtual int width() override { return mState.getWidth(); }
+    virtual int height() override { return mState.getHeight(); }
+
+    virtual void setHighContrastText(bool highContrastText) override {
+        mHighContrastText = highContrastText;
+    }
+    virtual bool isHighContrastText() override { return mHighContrastText; }
+
+// ----------------------------------------------------------------------------
+// android/graphics/Canvas state operations
+// ----------------------------------------------------------------------------
+    // Save (layer)
+    virtual int getSaveCount() const override { return mState.getSaveCount(); }
+    virtual int save(SkCanvas::SaveFlags flags) override;
+    virtual void restore() override;
+    virtual void restoreToCount(int saveCount) override;
+
+    virtual int saveLayer(float left, float top, float right, float bottom, const SkPaint* paint,
+        SkCanvas::SaveFlags flags) override;
+    virtual int saveLayerAlpha(float left, float top, float right, float bottom,
+            int alpha, SkCanvas::SaveFlags flags) override {
+        SkPaint paint;
+        paint.setAlpha(alpha);
+        return saveLayer(left, top, right, bottom, &paint, flags);
+    }
+
+    // Matrix
+    virtual void getMatrix(SkMatrix* outMatrix) const override { mState.getMatrix(outMatrix); }
+    virtual void setMatrix(const SkMatrix& matrix) override { mState.setMatrix(matrix); }
+
+    virtual void concat(const SkMatrix& matrix) override { mState.concatMatrix(matrix); }
+    virtual void rotate(float degrees) override;
+    virtual void scale(float sx, float sy) override;
+    virtual void skew(float sx, float sy) override;
+    virtual void translate(float dx, float dy) override;
+
+    // Clip
+    virtual bool getClipBounds(SkRect* outRect) const override;
+    virtual bool quickRejectRect(float left, float top, float right, float bottom) const override;
+    virtual bool quickRejectPath(const SkPath& path) const override;
+
+    virtual bool clipRect(float left, float top, float right, float bottom, SkRegion::Op op) override;
+    virtual bool clipPath(const SkPath* path, SkRegion::Op op) override;
+    virtual bool clipRegion(const SkRegion* region, SkRegion::Op op) override;
+
+    // Misc
+    virtual SkDrawFilter* getDrawFilter() override { return mDrawFilter.get(); }
+    virtual void setDrawFilter(SkDrawFilter* filter) override {
+        mDrawFilter.reset(SkSafeRef(filter));
+    }
+
+// ----------------------------------------------------------------------------
+// android/graphics/Canvas draw operations
+// ----------------------------------------------------------------------------
+    virtual void drawColor(int color, SkXfermode::Mode mode) override;
+    virtual void drawPaint(const SkPaint& paint) override;
+
+    // Geometry
+    virtual void drawPoint(float x, float y, const SkPaint& paint) override {
+        float points[2] = { x, y };
+        drawPoints(points, 2, paint);
+    }
+    virtual void drawPoints(const float* points, int count, 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 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,
+            float rx, float ry, const SkPaint& paint) override;
+    virtual void drawCircle(float x, float y, float radius, const SkPaint& paint) override;
+    virtual void drawOval(float left, float top, float right, float bottom, const SkPaint& paint) override;
+    virtual void drawArc(float left, float top, float right, float bottom,
+            float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) override;
+    virtual void drawPath(const SkPath& path, const SkPaint& paint) override;
+    virtual void drawVertices(SkCanvas::VertexMode vertexMode, int vertexCount,
+            const float* verts, const float* tex, const int* colors,
+            const uint16_t* indices, int indexCount, const SkPaint& paint) override
+        { /* RecordingCanvas does not support drawVertices(); ignore */ }
+
+    // Bitmap-based
+    virtual void drawBitmap(const SkBitmap& bitmap, float left, float top, const SkPaint* paint) override;
+    virtual void drawBitmap(const SkBitmap& bitmap, const SkMatrix& matrix,
+                            const SkPaint* paint) override;
+    virtual void drawBitmap(const SkBitmap& bitmap, float srcLeft, float srcTop,
+            float srcRight, float srcBottom, float dstLeft, float dstTop,
+            float dstRight, float dstBottom, const SkPaint* paint) override;
+    virtual void drawBitmapMesh(const SkBitmap& bitmap, int meshWidth, int meshHeight,
+            const float* vertices, const int* colors, const SkPaint* paint) override;
+    virtual void drawNinePatch(const SkBitmap& bitmap, const android::Res_png_9patch& chunk,
+            float dstLeft, float dstTop, float dstRight, float dstBottom,
+            const SkPaint* paint) override;
+
+    // Text
+    virtual void drawText(const uint16_t* glyphs, const float* positions, int count,
+            const SkPaint& paint, float x, float y, float boundsLeft, float boundsTop,
+            float boundsRight, float boundsBottom, float totalAdvance) override;
+    virtual void drawPosText(const uint16_t* text, const float* positions, int count,
+            int posCount, const SkPaint& paint) override;
+    virtual void drawTextOnPath(const uint16_t* glyphs, int count, const SkPath& path,
+            float hOffset, float vOffset, const SkPaint& paint) override;
+    virtual bool drawTextAbsolutePos() const override { return false; }
+
+private:
+    enum DeferredBarrierType {
+        kBarrier_None,
+        kBarrier_InOrder,
+        kBarrier_OutOfOrder,
+    };
+
+    void drawBitmap(const SkBitmap* bitmap, const SkPaint* paint);
+    void drawSimpleRects(const float* rects, int vertexCount, const SkPaint* paint);
+
+
+    size_t addOp(RecordedOp* op);
+// ----------------------------------------------------------------------------
+// lazy object copy
+// ----------------------------------------------------------------------------
+    LinearAllocator& alloc() { return mDisplayListData->allocator; }
+
+    void refBitmapsInShader(const SkShader* shader);
+
+    template<class T>
+    inline const T* refBuffer(const T* srcBuffer, int32_t count) {
+        if (!srcBuffer) return nullptr;
+
+        T* dstBuffer = (T*) mDisplayListData->allocator.alloc(count * sizeof(T));
+        memcpy(dstBuffer, srcBuffer, count * sizeof(T));
+        return dstBuffer;
+    }
+
+    inline char* refText(const char* text, size_t byteLength) {
+        return (char*) refBuffer<uint8_t>((uint8_t*)text, byteLength);
+    }
+
+    inline const SkPath* refPath(const SkPath* path) {
+        if (!path) return nullptr;
+
+        // The points/verbs within the path are refcounted so this copy operation
+        // is inexpensive and maintains the generationID of the original path.
+        const SkPath* cachedPath = new SkPath(*path);
+        mDisplayListData->pathResources.push_back(cachedPath);
+        return cachedPath;
+    }
+
+    inline const SkPaint* refPaint(const SkPaint* paint) {
+        if (!paint) return nullptr;
+
+        // If there is a draw filter apply it here and store the modified paint
+        // so that we don't need to modify the paint every time we access it.
+        SkTLazy<SkPaint> filteredPaint;
+        if (mDrawFilter.get()) {
+            filteredPaint.set(*paint);
+            mDrawFilter->filter(filteredPaint.get(), SkDrawFilter::kPaint_Type);
+            paint = filteredPaint.get();
+        }
+
+        // compute the hash key for the paint and check the cache.
+        const uint32_t key = paint->getHash();
+        const SkPaint* cachedPaint = mPaintMap.valueFor(key);
+        // In the unlikely event that 2 unique paints have the same hash we do a
+        // object equality check to ensure we don't erroneously dedup them.
+        if (cachedPaint == nullptr || *cachedPaint != *paint) {
+            cachedPaint = new SkPaint(*paint);
+            std::unique_ptr<const SkPaint> copy(cachedPaint);
+            mDisplayListData->paints.push_back(std::move(copy));
+
+            // replaceValueFor() performs an add if the entry doesn't exist
+            mPaintMap.replaceValueFor(key, cachedPaint);
+            refBitmapsInShader(cachedPaint->getShader());
+        }
+
+        return cachedPaint;
+    }
+
+    inline const SkRegion* refRegion(const SkRegion* region) {
+        if (!region) {
+            return region;
+        }
+
+        const SkRegion* cachedRegion = mRegionMap.valueFor(region);
+        // TODO: Add generation ID to SkRegion
+        if (cachedRegion == nullptr) {
+            std::unique_ptr<const SkRegion> copy(new SkRegion(*region));
+            cachedRegion = copy.get();
+            mDisplayListData->regions.push_back(std::move(copy));
+
+            // replaceValueFor() performs an add if the entry doesn't exist
+            mRegionMap.replaceValueFor(region, cachedRegion);
+        }
+
+        return cachedRegion;
+    }
+
+    inline const SkBitmap* refBitmap(const SkBitmap& bitmap) {
+        // Note that this assumes the bitmap is immutable. There are cases this won't handle
+        // correctly, such as creating the bitmap from scratch, drawing with it, changing its
+        // contents, and drawing again. The only fix would be to always copy it the first time,
+        // which doesn't seem worth the extra cycles for this unlikely case.
+        SkBitmap* localBitmap = new (alloc()) SkBitmap(bitmap);
+        alloc().autoDestroy(localBitmap);
+        mDisplayListData->bitmapResources.push_back(localBitmap);
+        return localBitmap;
+    }
+
+    inline const Res_png_9patch* refPatch(const Res_png_9patch* patch) {
+        mDisplayListData->patchResources.push_back(patch);
+        mResourceCache.incrementRefcount(patch);
+        return patch;
+    }
+
+    DefaultKeyedVector<uint32_t, const SkPaint*> mPaintMap;
+    DefaultKeyedVector<const SkPath*, const SkPath*> mPathMap;
+    DefaultKeyedVector<const SkRegion*, const SkRegion*> mRegionMap;
+
+    CanvasState mState;
+    std::unique_ptr<SkiaCanvasProxy> mSkiaCanvasProxy;
+    ResourceCache& mResourceCache;
+    DeferredBarrierType mDeferredBarrierType = kBarrier_None;
+    DisplayListData* mDisplayListData = nullptr;
+    bool mHighContrastText = false;
+    SkAutoTUnref<SkDrawFilter> mDrawFilter;
+    int mRestoreSaveCount = -1;
+}; // class RecordingCanvas
+
+}; // namespace uirenderer
+}; // namespace android
+
+#endif // ANDROID_HWUI_RECORDING_CANVAS_H
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index bf1b4d0..d122a55 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -25,6 +25,9 @@
 
 #include "DamageAccumulator.h"
 #include "Debug.h"
+#if HWUI_NEW_OPS
+#include "RecordedOp.h"
+#endif
 #include "DisplayListOp.h"
 #include "LayerRenderer.h"
 #include "OpenGLRenderer.h"
@@ -47,7 +50,7 @@
     }
     if (mDisplayListData) {
         for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
-            mDisplayListData->children()[i]->mRenderNode->debugDumpLayers(prefix);
+            mDisplayListData->children()[i]->renderNode->debugDumpLayers(prefix);
         }
     }
 }
@@ -172,7 +175,7 @@
     pnode->clear_children();
     if (mDisplayListData) {
         for (auto&& child : mDisplayListData->children()) {
-            child->mRenderNode->copyTo(pnode->add_children());
+            child->renderNode->copyTo(pnode->add_children());
         }
     }
 }
@@ -332,6 +335,10 @@
     info.damageAccumulator->popTransform();
 }
 
+void RenderNode::syncProperties() {
+    mProperties = mStagingProperties;
+}
+
 void RenderNode::pushStagingPropertiesChanges(TreeInfo& info) {
     // Push the animators first so that setupStartValueIfNecessary() is called
     // before properties() is trampled by stagingProperties(), as they are
@@ -343,7 +350,7 @@
         mDirtyPropertyFields = 0;
         damageSelf(info);
         info.damageAccumulator->popTransform();
-        mProperties = mStagingProperties;
+        syncProperties();
         applyLayerPropertiesToLayer(info);
         // We could try to be clever and only re-damage if the matrix changed.
         // However, we don't need to worry about that. The cost of over-damaging
@@ -364,35 +371,39 @@
     mLayer->setBlend(props.needsBlending());
 }
 
+void RenderNode::syncDisplayList() {
+    // Make sure we inc first so that we don't fluctuate between 0 and 1,
+    // which would thrash the layer cache
+    if (mStagingDisplayListData) {
+        for (auto&& child : mStagingDisplayListData->children()) {
+            child->renderNode->incParentRefCount();
+        }
+    }
+    deleteDisplayListData();
+    mDisplayListData = mStagingDisplayListData;
+    mStagingDisplayListData = nullptr;
+    if (mDisplayListData) {
+        for (size_t i = 0; i < mDisplayListData->functors.size(); i++) {
+            (*mDisplayListData->functors[i])(DrawGlInfo::kModeSync, nullptr);
+        }
+    }
+}
+
 void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) {
     if (mNeedsDisplayListDataSync) {
         mNeedsDisplayListDataSync = false;
-        // Make sure we inc first so that we don't fluctuate between 0 and 1,
-        // which would thrash the layer cache
-        if (mStagingDisplayListData) {
-            for (size_t i = 0; i < mStagingDisplayListData->children().size(); i++) {
-                mStagingDisplayListData->children()[i]->mRenderNode->incParentRefCount();
-            }
-        }
         // Damage with the old display list first then the new one to catch any
         // changes in isRenderable or, in the future, bounds
         damageSelf(info);
-        deleteDisplayListData();
-        mDisplayListData = mStagingDisplayListData;
-        mStagingDisplayListData = nullptr;
-        if (mDisplayListData) {
-            for (size_t i = 0; i < mDisplayListData->functors.size(); i++) {
-                (*mDisplayListData->functors[i])(DrawGlInfo::kModeSync, nullptr);
-            }
-        }
+        syncDisplayList();
         damageSelf(info);
     }
 }
 
 void RenderNode::deleteDisplayListData() {
     if (mDisplayListData) {
-        for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
-            mDisplayListData->children()[i]->mRenderNode->decParentRefCount();
+        for (auto&& child : mDisplayListData->children()) {
+            child->renderNode->decParentRefCount();
         }
     }
     delete mDisplayListData;
@@ -407,13 +418,17 @@
             info.prepareTextures = cache.prefetchAndMarkInUse(
                     info.canvasContext, subtree->bitmapResources[i]);
         }
-        for (size_t i = 0; i < subtree->children().size(); i++) {
-            DrawRenderNodeOp* op = subtree->children()[i];
-            RenderNode* childNode = op->mRenderNode;
+        for (auto&& op : subtree->children()) {
+            RenderNode* childNode = op->renderNode;
+#if HWUI_NEW_OPS
+            info.damageAccumulator->pushTransform(&op->localMatrix);
+            bool childFunctorsNeedLayer = functorsNeedLayer; // TODO! || op->mRecordedWithPotentialStencilClip;
+#else
             info.damageAccumulator->pushTransform(&op->mTransformFromParent);
             bool childFunctorsNeedLayer = functorsNeedLayer
                     // Recorded with non-rect clip, or canvas-rotated by parent
                     || op->mRecordedWithPotentialStencilClip;
+#endif
             childNode->prepareTreeImpl(info, childFunctorsNeedLayer);
             info.damageAccumulator->popTransform();
         }
@@ -426,8 +441,8 @@
         mLayer = nullptr;
     }
     if (mDisplayListData) {
-        for (size_t i = 0; i < mDisplayListData->children().size(); i++) {
-            mDisplayListData->children()[i]->mRenderNode->destroyHardwareResources();
+        for (auto&& child : mDisplayListData->children()) {
+            child->renderNode->destroyHardwareResources();
         }
         if (mNeedsDisplayListDataSync) {
             // Next prepare tree we are going to push a new display list, so we can
@@ -449,6 +464,34 @@
     }
 }
 
+bool RenderNode::applyViewProperties(CanvasState& canvasState) const {
+    const Outline& outline = properties().getOutline();
+    if (properties().getAlpha() <= 0
+            || (outline.getShouldClip() && outline.isEmpty())
+            || properties().getScaleX() == 0
+            || properties().getScaleY() == 0) {
+        return false; // rejected
+    }
+
+    if (properties().getLeft() != 0 || properties().getTop() != 0) {
+        canvasState.translate(properties().getLeft(), properties().getTop());
+    }
+    if (properties().getStaticMatrix()) {
+        canvasState.concatMatrix(*properties().getStaticMatrix());
+    } else if (properties().getAnimationMatrix()) {
+        canvasState.concatMatrix(*properties().getAnimationMatrix());
+    }
+    if (properties().hasTransformMatrix()) {
+        if (properties().isTransformTranslateOnly()) {
+            canvasState.translate(properties().getTranslationX(), properties().getTranslationY());
+        } else {
+            canvasState.concatMatrix(*properties().getTransformMatrix());
+        }
+    }
+    return !canvasState.quickRejectConservative(
+            0, 0, properties().getWidth(), properties().getHeight());
+}
+
 /*
  * For property operations, we pass a savecount of 0, since the operations aren't part of the
  * displaylist, and thus don't have to compensate for the record-time/playback-time discrepancy in
@@ -580,6 +623,7 @@
  * which are flagged to not draw in the standard draw loop.
  */
 void RenderNode::computeOrdering() {
+#if !HWUI_NEW_OPS
     ATRACE_CALL();
     mProjectedNodes.clear();
 
@@ -588,14 +632,16 @@
     if (mDisplayListData == nullptr) return;
     for (unsigned int i = 0; i < mDisplayListData->children().size(); i++) {
         DrawRenderNodeOp* childOp = mDisplayListData->children()[i];
-        childOp->mRenderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity());
+        childOp->renderNode->computeOrderingImpl(childOp, &mProjectedNodes, &mat4::identity());
     }
+#endif
 }
 
 void RenderNode::computeOrderingImpl(
         DrawRenderNodeOp* opState,
         std::vector<DrawRenderNodeOp*>* compositedChildrenOfProjectionSurface,
         const mat4* transformFromProjectionSurface) {
+#if !HWUI_NEW_OPS
     mProjectedNodes.clear();
     if (mDisplayListData == nullptr || mDisplayListData->isEmpty()) return;
 
@@ -619,7 +665,7 @@
         bool haveAppliedPropertiesToProjection = false;
         for (unsigned int i = 0; i < mDisplayListData->children().size(); i++) {
             DrawRenderNodeOp* childOp = mDisplayListData->children()[i];
-            RenderNode* child = childOp->mRenderNode;
+            RenderNode* child = childOp->renderNode;
 
             std::vector<DrawRenderNodeOp*>* projectionChildren = nullptr;
             const mat4* projectionTransform = nullptr;
@@ -642,6 +688,7 @@
             child->computeOrderingImpl(childOp, projectionChildren, projectionTransform);
         }
     }
+#endif
 }
 
 class DeferOperationHandler {
@@ -701,11 +748,12 @@
 
 void RenderNode::buildZSortedChildList(const DisplayListData::Chunk& chunk,
         std::vector<ZDrawRenderNodeOpPair>& zTranslatedNodes) {
+#if !HWUI_NEW_OPS
     if (chunk.beginChildIndex == chunk.endChildIndex) return;
 
     for (unsigned int i = chunk.beginChildIndex; i < chunk.endChildIndex; i++) {
         DrawRenderNodeOp* childOp = mDisplayListData->children()[i];
-        RenderNode* child = childOp->mRenderNode;
+        RenderNode* child = childOp->renderNode;
         float childZ = child->properties().getZ();
 
         if (!MathUtils::isZero(childZ) && chunk.reorderChildren) {
@@ -719,6 +767,7 @@
 
     // Z sort any 3d children (stable-ness makes z compare fall back to standard drawing order)
     std::stable_sort(zTranslatedNodes.begin(), zTranslatedNodes.end());
+#endif
 }
 
 template <class T>
@@ -824,7 +873,7 @@
     while (shadowIndex < endIndex || drawIndex < endIndex) {
         if (shadowIndex < endIndex) {
             DrawRenderNodeOp* casterOp = zTranslatedNodes[shadowIndex].value;
-            RenderNode* caster = casterOp->mRenderNode;
+            RenderNode* caster = casterOp->renderNode;
             const float casterZ = zTranslatedNodes[shadowIndex].key;
             // attempt to render the shadow if the caster about to be drawn is its caster,
             // OR if its caster's Z value is similar to the previous potential caster
@@ -869,7 +918,7 @@
     const DisplayListOp* op =
             (mDisplayListData->displayListOps[mDisplayListData->projectionReceiveIndex]);
     const DrawRenderNodeOp* backgroundOp = reinterpret_cast<const DrawRenderNodeOp*>(op);
-    const RenderProperties& backgroundProps = backgroundOp->mRenderNode->properties();
+    const RenderProperties& backgroundProps = backgroundOp->renderNode->properties();
     renderer.translate(backgroundProps.getTranslationX(), backgroundProps.getTranslationY());
 
     // If the projection reciever has an outline, we mask projected content to it
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 88fc608..ff673ba 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -29,8 +29,8 @@
 
 #include "AnimatorManager.h"
 #include "Debug.h"
-#include "Matrix.h"
 #include "DisplayList.h"
+#include "Matrix.h"
 #include "RenderProperties.h"
 
 #include <vector>
@@ -43,6 +43,7 @@
 namespace android {
 namespace uirenderer {
 
+class CanvasState;
 class DisplayListOp;
 class DisplayListCanvas;
 class OpenGLRenderer;
@@ -74,6 +75,7 @@
  * attached.
  */
 class RenderNode : public VirtualLightRefBase {
+friend class TestUtils; // allow TestUtils to access syncDisplayList / syncProperties
 public:
     enum DirtyPropertyMask {
         GENERIC         = 1 << 1,
@@ -176,8 +178,25 @@
 
     AnimatorManager& animators() { return mAnimatorManager; }
 
+    // Returns false if the properties dictate the subtree contained in this RenderNode won't render
+    bool applyViewProperties(CanvasState& canvasState) const;
+
     void applyViewPropertyTransforms(mat4& matrix, bool true3dTransform = false) const;
 
+    bool nothingToDraw() const {
+        const Outline& outline = properties().getOutline();
+        return mDisplayListData == nullptr
+                || properties().getAlpha() <= 0
+                || (outline.getShouldClip() && outline.isEmpty())
+                || properties().getScaleX() == 0
+                || properties().getScaleY() == 0;
+    }
+
+    // Only call if RenderNode has DisplayListData...
+    const DisplayListData& getDisplayListData() const {
+        return *mDisplayListData;
+    }
+
 private:
     typedef key_value_pair_t<float, DrawRenderNodeOp*> ZDrawRenderNodeOpPair;
 
@@ -235,6 +254,10 @@
         const char* mText;
     };
 
+
+    void syncProperties();
+    void syncDisplayList();
+
     void prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer);
     void pushStagingPropertiesChanges(TreeInfo& info);
     void pushStagingDisplayListChanges(TreeInfo& info);
diff --git a/libs/hwui/microbench/DisplayListCanvasBench.cpp b/libs/hwui/microbench/DisplayListCanvasBench.cpp
index 7b6be7a..fd42adb 100644
--- a/libs/hwui/microbench/DisplayListCanvasBench.cpp
+++ b/libs/hwui/microbench/DisplayListCanvasBench.cpp
@@ -18,12 +18,22 @@
 #include <utils/Singleton.h>
 
 #include "DisplayList.h"
+#if HWUI_NEW_OPS
+#include "RecordingCanvas.h"
+#else
 #include "DisplayListCanvas.h"
+#endif
 #include "microbench/MicroBench.h"
 
 using namespace android;
 using namespace android::uirenderer;
 
+#if HWUI_NEW_OPS
+typedef RecordingCanvas TestCanvas;
+#else
+typedef DisplayListCanvas TestCanvas;
+#endif
+
 BENCHMARK_NO_ARG(BM_DisplayListData_alloc);
 void BM_DisplayListData_alloc::Run(int iters) {
     StartBenchmarkTiming();
@@ -48,7 +58,7 @@
 
 BENCHMARK_NO_ARG(BM_DisplayListCanvas_record_empty);
 void BM_DisplayListCanvas_record_empty::Run(int iters) {
-    DisplayListCanvas canvas(100, 100);
+    TestCanvas canvas(100, 100);
     canvas.finishRecording();
 
     StartBenchmarkTiming();
@@ -62,7 +72,7 @@
 
 BENCHMARK_NO_ARG(BM_DisplayListCanvas_record_saverestore);
 void BM_DisplayListCanvas_record_saverestore::Run(int iters) {
-    DisplayListCanvas canvas(100, 100);
+    TestCanvas canvas(100, 100);
     canvas.finishRecording();
 
     StartBenchmarkTiming();
@@ -80,7 +90,7 @@
 
 BENCHMARK_NO_ARG(BM_DisplayListCanvas_record_translate);
 void BM_DisplayListCanvas_record_translate::Run(int iters) {
-    DisplayListCanvas canvas(100, 100);
+    TestCanvas canvas(100, 100);
     canvas.finishRecording();
 
     StartBenchmarkTiming();
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 38f6e53..e1d8abd 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -28,6 +28,11 @@
 #include "renderstate/Stencil.h"
 #include "protos/hwui.pb.h"
 
+#if HWUI_NEW_OPS
+#include "BakedOpRenderer.h"
+#include "OpReorderer.h"
+#endif
+
 #include <cutils/properties.h>
 #include <google/protobuf/io/zero_copy_stream_impl.h>
 #include <private/hwui/DrawGlInfo.h>
@@ -121,9 +126,11 @@
 
 bool CanvasContext::initialize(ANativeWindow* window) {
     setSurface(window);
+#if !HWUI_NEW_OPS
     if (mCanvas) return false;
     mCanvas = new OpenGLRenderer(mRenderThread.renderState());
     mCanvas->initProperties();
+#endif
     return true;
 }
 
@@ -162,11 +169,13 @@
 }
 
 void CanvasContext::processLayerUpdate(DeferredLayerUpdater* layerUpdater) {
+#if !HWUI_NEW_OPS
     bool success = layerUpdater->apply();
     LOG_ALWAYS_FATAL_IF(!success, "Failed to update layer!");
     if (layerUpdater->backingLayer()->deferredUpdateScheduled) {
         mCanvas->pushLayerUpdate(layerUpdater->backingLayer());
     }
+#endif
 }
 
 static bool wasSkipped(FrameInfo* info) {
@@ -239,8 +248,10 @@
 }
 
 void CanvasContext::draw() {
+#if !HWUI_NEW_OPS
     LOG_ALWAYS_FATAL_IF(!mCanvas || mEglSurface == EGL_NO_SURFACE,
             "drawRenderNode called on a context with no canvas or surface!");
+#endif
 
     SkRect dirty;
     mDamageAccumulator.finish(&dirty);
@@ -254,6 +265,8 @@
     mCurrentFrameInfo->markIssueDrawCommandsStart();
 
     Frame frame = mEglManager.beginFrame(mEglSurface);
+
+#if !HWUI_NEW_OPS
     if (frame.width() != mCanvas->getViewportWidth()
             || frame.height() != mCanvas->getViewportHeight()) {
         // can't rely on prior content of window if viewport size changes
@@ -417,6 +430,16 @@
     // Even if we decided to cancel the frame, from the perspective of jank
     // metrics the frame was swapped at this point
     mCurrentFrameInfo->markSwapBuffers();
+#else
+    OpReorderer reorderer;
+    reorderer.defer(frame.width(), frame.height(), mRenderNodes);
+    BakedOpRenderer::Info info(Caches::getInstance(), mRenderThread.renderState(),
+            frame.width(), frame.height(), mOpaque);
+    reorderer.replayBakedOps<BakedOpRenderer>(&info);
+
+    bool drew = info.didDraw;
+    SkRect screenDirty = SkRect::MakeWH(frame.width(), frame.height());
+#endif
 
     if (drew) {
         if (CC_UNLIKELY(!mEglManager.swapBuffers(frame, screenDirty))) {
diff --git a/libs/hwui/tests/TreeContentAnimation.cpp b/libs/hwui/tests/TreeContentAnimation.cpp
index a59261c..891af91 100644
--- a/libs/hwui/tests/TreeContentAnimation.cpp
+++ b/libs/hwui/tests/TreeContentAnimation.cpp
@@ -20,6 +20,7 @@
 
 #include <AnimationContext.h>
 #include <DisplayListCanvas.h>
+#include <RecordingCanvas.h>
 #include <RenderNode.h>
 #include <renderthread/RenderProxy.h>
 #include <renderthread/RenderTask.h>
@@ -39,6 +40,13 @@
 using namespace android::uirenderer::renderthread;
 using namespace android::uirenderer::test;
 
+#if HWUI_NEW_OPS
+typedef RecordingCanvas TestCanvas;
+#else
+typedef DisplayListCanvas TestCanvas;
+#endif
+
+
 class ContextFactory : public IContextFactory {
 public:
     virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override {
@@ -46,13 +54,13 @@
     }
 };
 
-static DisplayListCanvas* startRecording(RenderNode* node) {
-    DisplayListCanvas* renderer = new DisplayListCanvas(
+static TestCanvas* startRecording(RenderNode* node) {
+    TestCanvas* renderer = new TestCanvas(
             node->stagingProperties().getWidth(), node->stagingProperties().getHeight());
     return renderer;
 }
 
-static void endRecording(DisplayListCanvas* renderer, RenderNode* node) {
+static void endRecording(TestCanvas* renderer, RenderNode* node) {
     node->setStagingDisplayList(renderer->finishRecording());
     delete renderer;
 }
@@ -67,7 +75,7 @@
             frameCount = fc;
         }
     }
-    virtual void createContent(int width, int height, DisplayListCanvas* renderer) = 0;
+    virtual void createContent(int width, int height, TestCanvas* renderer) = 0;
     virtual void doFrame(int frameNr) = 0;
 
     template <class T>
@@ -102,7 +110,7 @@
 
         android::uirenderer::Rect DUMMY;
 
-        DisplayListCanvas* renderer = startRecording(rootNode);
+        TestCanvas* renderer = startRecording(rootNode);
         animation.createContent(width, height, renderer);
         endRecording(renderer, rootNode);
 
@@ -132,7 +140,7 @@
 class ShadowGridAnimation : public TreeContentAnimation {
 public:
     std::vector< sp<RenderNode> > cards;
-    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+    void createContent(int width, int height, TestCanvas* renderer) override {
         renderer->drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
         renderer->insertReorderBarrier(true);
 
@@ -163,7 +171,7 @@
         node->mutateStagingProperties().mutableOutline().setShouldClip(true);
         node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y | RenderNode::Z);
 
-        DisplayListCanvas* renderer = startRecording(node.get());
+        TestCanvas* renderer = startRecording(node.get());
         renderer->drawColor(0xFFEEEEEE, SkXfermode::kSrcOver_Mode);
         endRecording(renderer, node.get());
         return node;
@@ -179,7 +187,7 @@
 class ShadowGrid2Animation : public TreeContentAnimation {
 public:
     std::vector< sp<RenderNode> > cards;
-    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+    void createContent(int width, int height, TestCanvas* renderer) override {
         renderer->drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
         renderer->insertReorderBarrier(true);
 
@@ -210,7 +218,7 @@
         node->mutateStagingProperties().mutableOutline().setShouldClip(true);
         node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y | RenderNode::Z);
 
-        DisplayListCanvas* renderer = startRecording(node.get());
+        TestCanvas* renderer = startRecording(node.get());
         renderer->drawColor(0xFFEEEEEE, SkXfermode::kSrcOver_Mode);
         endRecording(renderer, node.get());
         return node;
@@ -226,7 +234,7 @@
 class RectGridAnimation : public TreeContentAnimation {
 public:
     sp<RenderNode> card;
-    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+    void createContent(int width, int height, TestCanvas* renderer) override {
         renderer->drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
         renderer->insertReorderBarrier(true);
 
@@ -247,7 +255,7 @@
         node->mutateStagingProperties().setLeftTopRightBottom(x, y, x + width, y + height);
         node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
 
-        DisplayListCanvas* renderer = startRecording(node.get());
+        TestCanvas* renderer = startRecording(node.get());
         renderer->drawColor(0xFFFF00FF, SkXfermode::kSrcOver_Mode);
 
         SkRegion region;
@@ -275,7 +283,7 @@
 class OvalAnimation : public TreeContentAnimation {
 public:
     sp<RenderNode> card;
-    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+    void createContent(int width, int height, TestCanvas* renderer) override {
         renderer->drawColor(0xFFFFFFFF, SkXfermode::kSrcOver_Mode);
         renderer->insertReorderBarrier(true);
 
@@ -297,7 +305,7 @@
         node->mutateStagingProperties().setLeftTopRightBottom(x, y, x + width, y + height);
         node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
 
-        DisplayListCanvas* renderer = startRecording(node.get());
+        TestCanvas* renderer = startRecording(node.get());
 
         SkPaint paint;
         paint.setAntiAlias(true);
@@ -317,7 +325,7 @@
 class PartialDamageTest : public TreeContentAnimation {
 public:
     std::vector< sp<RenderNode> > cards;
-    void createContent(int width, int height, DisplayListCanvas* renderer) override {
+    void createContent(int width, int height, TestCanvas* renderer) override {
         static SkColor COLORS[] = {
                 0xFFF44336,
                 0xFF9C27B0,
@@ -342,7 +350,7 @@
         cards[0]->mutateStagingProperties().setTranslationY(curFrame);
         cards[0]->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
 
-        DisplayListCanvas* renderer = startRecording(cards[0].get());
+        TestCanvas* renderer = startRecording(cards[0].get());
         renderer->drawColor(interpolateColor(curFrame / 150.0f, 0xFFF44336, 0xFFF8BBD0),
                 SkXfermode::kSrcOver_Mode);
         endRecording(renderer, cards[0].get());
@@ -370,7 +378,7 @@
         node->mutateStagingProperties().setLeftTopRightBottom(x, y, x + width, y + height);
         node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
 
-        DisplayListCanvas* renderer = startRecording(node.get());
+        TestCanvas* renderer = startRecording(node.get());
         renderer->drawColor(color, SkXfermode::kSrcOver_Mode);
         endRecording(renderer, node.get());
         return node;
@@ -383,3 +391,43 @@
     "EGL_KHR_partial_update is supported by the device & are enabled in hwui.",
     TreeContentAnimation::run<PartialDamageTest>
 });
+
+
+class SimpleRectGridAnimation : public TreeContentAnimation {
+public:
+    sp<RenderNode> card;
+    void createContent(int width, int height, TestCanvas* renderer) override {
+        SkPaint paint;
+        paint.setColor(0xFF00FFFF);
+        renderer->drawRect(0, 0, width, height, paint);
+
+        card = createCard(40, 40, 200, 200);
+        renderer->drawRenderNode(card.get());
+    }
+    void doFrame(int frameNr) override {
+        int curFrame = frameNr % 150;
+        card->mutateStagingProperties().setTranslationX(curFrame);
+        card->mutateStagingProperties().setTranslationY(curFrame);
+        card->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
+    }
+private:
+    sp<RenderNode> createCard(int x, int y, int width, int height) {
+        sp<RenderNode> node = new RenderNode();
+        node->mutateStagingProperties().setLeftTopRightBottom(x, y, x + width, y + height);
+        node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
+
+        TestCanvas* renderer = startRecording(node.get());
+        SkPaint paint;
+        paint.setColor(0xFFFF00FF);
+        renderer->drawRect(0, 0, width, height, paint);
+
+        endRecording(renderer, node.get());
+        return node;
+    }
+};
+static Benchmark _SimpleRectGrid(BenchmarkInfo{
+    "simplerectgrid",
+    "A simple collection of rects. "
+    "Low CPU/GPU load.",
+    TreeContentAnimation::run<SimpleRectGridAnimation>
+});
diff --git a/libs/hwui/unit_tests/BakedOpStateTests.cpp b/libs/hwui/unit_tests/BakedOpStateTests.cpp
new file mode 100644
index 0000000..82aebea
--- /dev/null
+++ b/libs/hwui/unit_tests/BakedOpStateTests.cpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include <BakedOpState.h>
+#include <RecordedOp.h>
+#include <unit_tests/TestUtils.h>
+
+namespace android {
+namespace uirenderer {
+
+TEST(ResolvedRenderState, resolution) {
+    Matrix4 identity;
+    identity.loadIdentity();
+
+    Matrix4 translate10x20;
+    translate10x20.loadTranslate(10, 20, 0);
+
+    SkPaint paint;
+    RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, Rect(0, 0, 100, 200), &paint);
+    {
+        // recorded with transform, no parent transform
+        auto parentSnapshot = TestUtils::makeSnapshot(identity, Rect(0, 0, 100, 200));
+        ResolvedRenderState state(*parentSnapshot, recordedOp);
+        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
+    }
+    {
+        // recorded with transform and parent transform
+        auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(0, 0, 100, 200));
+        ResolvedRenderState state(*parentSnapshot, recordedOp);
+
+        Matrix4 expectedTranslate;
+        expectedTranslate.loadTranslate(20, 40, 0);
+        EXPECT_MATRIX_APPROX_EQ(state.transform, expectedTranslate);
+
+        // intersection of parent & transformed child clip
+        EXPECT_EQ(state.clipRect, Rect(10, 20, 100, 200));
+
+        // translated and also clipped
+        EXPECT_EQ(state.clippedBounds, Rect(50, 80, 100, 200));
+    }
+}
+
+TEST(BakedOpState, constructAndReject) {
+    LinearAllocator allocator;
+
+    Matrix4 identity;
+    identity.loadIdentity();
+
+    Matrix4 translate100x0;
+    translate100x0.loadTranslate(100, 0, 0);
+
+    SkPaint paint;
+    {
+        RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, Rect(0, 0, 100, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(identity, Rect(0, 0, 100, 200));
+        BakedOpState* bakedOp = 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
+    }
+    {
+        RectOp successOp(Rect(30, 40, 100, 200), identity, Rect(0, 0, 100, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(identity, Rect(0, 0, 100, 200));
+        BakedOpState* bakedOp = 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
+    }
+}
+
+#define UNSUPPORTED_OP(Info, Type) \
+        static void on##Type(Info*, const Type&, const BakedOpState&) { FAIL(); }
+
+class Info {
+public:
+    int index = 0;
+};
+
+}
+}
diff --git a/libs/hwui/unit_tests/ClipAreaTests.cpp b/libs/hwui/unit_tests/ClipAreaTests.cpp
index 0c5e5e7..d6192df 100644
--- a/libs/hwui/unit_tests/ClipAreaTests.cpp
+++ b/libs/hwui/unit_tests/ClipAreaTests.cpp
@@ -101,10 +101,9 @@
     EXPECT_FALSE(area.isEmpty());
     EXPECT_FALSE(area.isSimple());
     EXPECT_FALSE(area.isRectangleList());
+
     Rect clipRect(area.getClipRect());
-    clipRect.dump("clipRect");
     Rect expected(0, 0, r * 2, r * 2);
-    expected.dump("expected");
     EXPECT_EQ(expected, clipRect);
     SkRegion clipRegion(area.getClipRegion());
     auto skRect(clipRegion.getBounds());
diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp
new file mode 100644
index 0000000..fcaea1e
--- /dev/null
+++ b/libs/hwui/unit_tests/OpReordererTests.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include <BakedOpState.h>
+#include <OpReorderer.h>
+#include <RecordedOp.h>
+#include <RecordingCanvas.h>
+#include <unit_tests/TestUtils.h>
+
+#include <unordered_map>
+
+namespace android {
+namespace uirenderer {
+
+#define UNSUPPORTED_OP(Info, Type) \
+        static void on##Type(Info*, const Type&, const BakedOpState&) { FAIL(); }
+
+class Info {
+public:
+    int index = 0;
+};
+
+class SimpleReceiver {
+public:
+    static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
+        EXPECT_EQ(1, info->index++);
+    }
+    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
+        EXPECT_EQ(0, info->index++);
+    }
+    UNSUPPORTED_OP(Info, RenderNodeOp)
+    UNSUPPORTED_OP(Info, SimpleRectsOp)
+    static void startFrame(Info& info) {}
+    static void endFrame(Info& info) {}
+};
+TEST(OpReorderer, simple) {
+    auto dld = TestUtils::createDLD<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+        SkBitmap bitmap;
+        bitmap.setInfo(SkImageInfo::MakeUnknown(25, 25));
+
+        canvas.drawRect(0, 0, 100, 200, SkPaint());
+        canvas.drawBitmap(bitmap, 10, 10, nullptr);
+    });
+
+    OpReorderer reorderer;
+    reorderer.defer(200, 200, dld->getChunks(), dld->getOps());
+
+    Info info;
+    reorderer.replayBakedOps<SimpleReceiver>(&info);
+}
+
+
+static int SIMPLE_BATCHING_LOOPS = 5;
+class SimpleBatchingReceiver {
+public:
+    static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) {
+        EXPECT_TRUE(info->index++ >= SIMPLE_BATCHING_LOOPS);
+    }
+    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
+        EXPECT_TRUE(info->index++ < SIMPLE_BATCHING_LOOPS);
+    }
+    UNSUPPORTED_OP(Info, RenderNodeOp)
+    UNSUPPORTED_OP(Info, SimpleRectsOp)
+    static void startFrame(Info& info) {}
+    static void endFrame(Info& info) {}
+};
+TEST(OpReorderer, simpleBatching) {
+    auto dld = TestUtils::createDLD<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) {
+        SkBitmap bitmap;
+        bitmap.setInfo(SkImageInfo::MakeUnknown(10, 10));
+
+        // 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.
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        for (int i = 0; i < SIMPLE_BATCHING_LOOPS; i++) {
+            canvas.translate(0, 10);
+            canvas.drawRect(0, 0, 10, 10, SkPaint());
+            canvas.drawBitmap(bitmap, 5, 0, nullptr);
+        }
+        canvas.restore();
+    });
+
+    OpReorderer reorderer;
+    reorderer.defer(200, 200, dld->getChunks(), dld->getOps());
+
+    Info info;
+    reorderer.replayBakedOps<SimpleBatchingReceiver>(&info);
+    EXPECT_EQ(2 * SIMPLE_BATCHING_LOOPS, info.index); // 2 x loops ops, because no merging (TODO: force no merging)
+}
+
+class RenderNodeReceiver {
+public:
+    UNSUPPORTED_OP(Info, BitmapOp)
+    static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) {
+        switch(info->index++) {
+        case 0:
+            EXPECT_EQ(Rect(0, 0, 200, 200), state.computedState.clippedBounds);
+            EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor());
+            break;
+        case 1:
+            EXPECT_EQ(Rect(50, 50, 150, 150), state.computedState.clippedBounds);
+            EXPECT_EQ(SK_ColorWHITE, op.paint->getColor());
+            break;
+        default:
+            FAIL();
+        }
+    }
+    UNSUPPORTED_OP(Info, RenderNodeOp)
+    UNSUPPORTED_OP(Info, SimpleRectsOp)
+    static void startFrame(Info& info) {}
+    static void endFrame(Info& info) {}
+};
+TEST(OpReorderer, renderNode) {
+    sp<RenderNode> child = TestUtils::createNode<RecordingCanvas>(10, 10, 110, 110, [](RecordingCanvas& canvas) {
+        SkPaint paint;
+        paint.setColor(SK_ColorWHITE);
+        canvas.drawRect(0, 0, 100, 100, paint);
+    });
+
+    RenderNode* childPtr = child.get();
+    sp<RenderNode> parent = TestUtils::createNode<RecordingCanvas>(0, 0, 200, 200, [childPtr](RecordingCanvas& canvas) {
+            SkPaint paint;
+            paint.setColor(SK_ColorDKGRAY);
+            canvas.drawRect(0, 0, 200, 200, paint);
+
+            canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+            canvas.translate(40, 40);
+            canvas.drawRenderNode(childPtr);
+            canvas.restore();
+    });
+
+    TestUtils::syncNodePropertiesAndDisplayList(child);
+    TestUtils::syncNodePropertiesAndDisplayList(parent);
+
+    std::vector< sp<RenderNode> > nodes;
+    nodes.push_back(parent.get());
+
+    OpReorderer reorderer;
+    reorderer.defer(200, 200, nodes);
+
+    Info info;
+    reorderer.replayBakedOps<RenderNodeReceiver>(&info);
+}
+
+}
+}
diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
new file mode 100644
index 0000000..c813833
--- /dev/null
+++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include <RecordedOp.h>
+#include <RecordingCanvas.h>
+#include <unit_tests/TestUtils.h>
+
+namespace android {
+namespace uirenderer {
+
+static void playbackOps(const std::vector<DisplayListData::Chunk>& chunks,
+        const std::vector<RecordedOp*>& ops, std::function<void(const RecordedOp&)> opReciever) {
+    for (const DisplayListData::Chunk& chunk : chunks) {
+        for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) {
+            opReciever(*ops[opIndex]);
+        }
+    }
+}
+
+TEST(RecordingCanvas, emptyPlayback) {
+    auto dld = TestUtils::createDLD<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        canvas.restore();
+    });
+    playbackOps(dld->getChunks(), dld->getOps(), [](const RecordedOp& op) { FAIL(); });
+}
+
+TEST(RecordingCanvas, testSimpleRectRecord) {
+    auto dld = TestUtils::createDLD<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+        canvas.drawRect(10, 20, 90, 180, SkPaint());
+    });
+
+    int count = 0;
+    playbackOps(dld->getChunks(), dld->getOps(), [&count](const RecordedOp& op) {
+        count++;
+        ASSERT_EQ(RecordedOpId::RectOp, op.opId);
+        ASSERT_EQ(Rect(0, 0, 100, 200), op.localClipRect);
+        ASSERT_EQ(Rect(10, 20, 90, 180), op.unmappedBounds);
+    });
+    ASSERT_EQ(1, count); // only one observed
+}
+
+TEST(RecordingCanvas, backgroundAndImage) {
+    auto dld = TestUtils::createDLD<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
+        SkBitmap bitmap;
+        bitmap.setInfo(SkImageInfo::MakeUnknown(25, 25));
+        SkPaint paint;
+        paint.setColor(SK_ColorBLUE);
+
+        canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+        {
+            // a background!
+            canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+            canvas.drawRect(0, 0, 100, 200, paint);
+            canvas.restore();
+        }
+        {
+            // an image!
+            canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag);
+            canvas.translate(25, 25);
+            canvas.scale(2, 2);
+            canvas.drawBitmap(bitmap, 0, 0, nullptr);
+            canvas.restore();
+        }
+        canvas.restore();
+    });
+
+    int count = 0;
+    playbackOps(dld->getChunks(), dld->getOps(), [&count](const RecordedOp& op) {
+        if (count == 0) {
+            ASSERT_EQ(RecordedOpId::RectOp, op.opId);
+            ASSERT_NE(nullptr, op.paint);
+            EXPECT_EQ(SK_ColorBLUE, op.paint->getColor());
+            EXPECT_EQ(Rect(0, 0, 100, 200), op.unmappedBounds);
+            EXPECT_EQ(Rect(0, 0, 100, 200), op.localClipRect);
+
+            Matrix4 expectedMatrix;
+            expectedMatrix.loadIdentity();
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+        } else {
+            ASSERT_EQ(RecordedOpId::BitmapOp, op.opId);
+            EXPECT_EQ(nullptr, op.paint);
+            EXPECT_EQ(Rect(0, 0, 25, 25), op.unmappedBounds);
+            EXPECT_EQ(Rect(0, 0, 100, 200), op.localClipRect);
+
+            Matrix4 expectedMatrix;
+            expectedMatrix.loadTranslate(25, 25, 0);
+            expectedMatrix.scale(2, 2, 1);
+            EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix);
+        }
+        count++;
+    });
+    ASSERT_EQ(2, count); // two draws observed
+}
+
+}
+}
diff --git a/libs/hwui/unit_tests/TestUtils.h b/libs/hwui/unit_tests/TestUtils.h
new file mode 100644
index 0000000..257dd28
--- /dev/null
+++ b/libs/hwui/unit_tests/TestUtils.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef TEST_UTILS_H
+#define TEST_UTILS_H
+
+#include <Matrix.h>
+#include <Snapshot.h>
+#include <RenderNode.h>
+
+#include <memory>
+
+namespace android {
+namespace uirenderer {
+
+#define EXPECT_MATRIX_APPROX_EQ(a, b) \
+    EXPECT_TRUE(TestUtils::matricesAreApproxEqual(a, b))
+
+class TestUtils {
+public:
+    static bool matricesAreApproxEqual(const Matrix4& a, const Matrix4& b) {
+        for (int i = 0; i < 16; i++) {
+            if (!MathUtils::areEqual(a[i], b[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static std::unique_ptr<Snapshot> makeSnapshot(const Matrix4& transform, const Rect& clip) {
+        std::unique_ptr<Snapshot> snapshot(new Snapshot());
+        snapshot->clip(clip.left, clip.top, clip.right, clip.bottom, SkRegion::kReplace_Op);
+        *(snapshot->transform) = transform;
+        return snapshot;
+    }
+
+    template<class CanvasType>
+    static std::unique_ptr<DisplayListData> createDLD(int width, int height,
+            std::function<void(CanvasType& canvas)> canvasCallback) {
+        CanvasType canvas(width, height);
+        canvasCallback(canvas);
+        return std::unique_ptr<DisplayListData>(canvas.finishRecording());
+    }
+
+    template<class CanvasType>
+    static sp<RenderNode> createNode(int left, int top, int right, int bottom,
+            std::function<void(CanvasType& canvas)> canvasCallback) {
+        sp<RenderNode> node = new RenderNode();
+        node->mutateStagingProperties().setLeftTopRightBottom(left, top, right, bottom);
+        node->setPropertyFieldsDirty(RenderNode::X | RenderNode::Y);
+
+        CanvasType canvas(
+                node->stagingProperties().getWidth(), node->stagingProperties().getHeight());
+        canvasCallback(canvas);
+        node->setStagingDisplayList(canvas.finishRecording());
+        return node;
+    }
+
+    static void syncNodePropertiesAndDisplayList(sp<RenderNode>& node) {
+        node->syncProperties();
+        node->syncDisplayList();
+    }
+}; // class TestUtils
+
+} /* namespace uirenderer */
+} /* namespace android */
+
+#endif /* TEST_UTILS_H */
diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h
index d00236e..db53713 100644
--- a/libs/hwui/utils/PaintUtils.h
+++ b/libs/hwui/utils/PaintUtils.h
@@ -20,6 +20,7 @@
 
 #include <SkColorFilter.h>
 #include <SkDrawLooper.h>
+#include <SkShader.h>
 #include <SkXfermode.h>
 
 namespace android {