DL-next groundwork

Initial groundwork / tests for the op-forward displaylist

Test: hwuiunit --gtest_filter=CanvasOpBuffer.*
Change-Id: I6a09d9841c964a67fde8203b979de3fd3fbd2026
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 155bb6b..4ed5457 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -429,6 +429,8 @@
     whole_static_libs: ["libskia"],
 
     srcs: [
+        "canvas/CanvasOpBuffer.cpp",
+        "canvas/CanvasOpRasterizer.cpp",
         "pipeline/skia/SkiaDisplayList.cpp",
         "pipeline/skia/SkiaRecordingCanvas.cpp",
         "pipeline/skia/RenderNodeDrawable.cpp",
@@ -604,6 +606,7 @@
         "tests/unit/ABitmapTests.cpp",
         "tests/unit/CacheManagerTests.cpp",
         "tests/unit/CanvasContextTests.cpp",
+        "tests/unit/CanvasOpTests.cpp",
         "tests/unit/CommonPoolTests.cpp",
         "tests/unit/DamageAccumulatorTests.cpp",
         "tests/unit/DeferredLayerUpdaterTests.cpp",
diff --git a/libs/hwui/canvas/CanvasOpBuffer.cpp b/libs/hwui/canvas/CanvasOpBuffer.cpp
new file mode 100644
index 0000000..7054e47e
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOpBuffer.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 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 "CanvasOpBuffer.h"
+
+#include "CanvasOps.h"
+
+namespace android::uirenderer {
+
+template class OpBuffer<CanvasOpType, CanvasOpContainer>;
+
+}  // namespace android::uirenderer
diff --git a/libs/hwui/canvas/CanvasOpBuffer.h b/libs/hwui/canvas/CanvasOpBuffer.h
new file mode 100644
index 0000000..b80faeb
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOpBuffer.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <SkMatrix.h>
+
+#include "CanvasOpTypes.h"
+#include "OpBuffer.h"
+
+namespace android::uirenderer {
+
+template <CanvasOpType T>
+struct CanvasOp;
+
+template <CanvasOpType T>
+class CanvasOpContainer {
+private:
+    BE_OPBUFFERS_FRIEND();
+
+    OpBufferItemHeader<CanvasOpType> header;
+    // TODO: Figure out some magic to make this not be here when it's identity (or not used)
+    SkMatrix mTransform;
+    CanvasOp<T> mImpl;
+
+public:
+    CanvasOpContainer(CanvasOp<T>&& impl, const SkMatrix& transform = SkMatrix::I())
+            : mTransform(transform), mImpl(std::move(impl)) {}
+
+    uint32_t size() const { return header.size; }
+    CanvasOpType type() const { return header.type; }
+
+    const SkMatrix& transform() const { return mTransform; }
+
+    CanvasOp<T>* operator->() noexcept { return &mImpl; }
+};
+
+extern template class OpBuffer<CanvasOpType, CanvasOpContainer>;
+class CanvasOpBuffer final : public OpBuffer<CanvasOpType, CanvasOpContainer> {
+public:
+    template <CanvasOpType T>
+    void push(CanvasOp<T>&& op) {
+        push_container(CanvasOpContainer<T>(std::move(op)));
+    }
+};
+
+}  // namespace android::uirenderer
diff --git a/libs/hwui/canvas/CanvasOpRasterizer.cpp b/libs/hwui/canvas/CanvasOpRasterizer.cpp
new file mode 100644
index 0000000..97c418a
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOpRasterizer.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 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 "CanvasOpRasterizer.h"
+
+#include <SkCanvas.h>
+#include <log/log.h>
+
+#include <vector>
+
+#include "CanvasOpBuffer.h"
+#include "CanvasOps.h"
+
+namespace android::uirenderer {
+
+void rasterizeCanvasBuffer(const CanvasOpBuffer& source, SkCanvas* destination) {
+    // Tracks the global transform from the current display list back toward the display space
+    // Push on beginning a RenderNode draw, pop on ending one
+    std::vector<SkMatrix> globalMatrixStack;
+    SkMatrix& currentGlobalTransform = globalMatrixStack.emplace_back(SkMatrix::I());
+
+    source.for_each([&]<CanvasOpType T>(CanvasOpContainer<T> * op) {
+        if constexpr (T == CanvasOpType::BeginZ || T == CanvasOpType::EndZ) {
+            // Do beginZ or endZ
+            LOG_ALWAYS_FATAL("TODO");
+            return;
+        } else {
+            // Generic OP
+            // First apply the current transformation
+            destination->setMatrix(SkMatrix::Concat(currentGlobalTransform, op->transform()));
+            // Now draw it
+            (*op)->draw(destination);
+        }
+    });
+}
+
+}  // namespace android::uirenderer
diff --git a/libs/hwui/canvas/CanvasOpRasterizer.h b/libs/hwui/canvas/CanvasOpRasterizer.h
new file mode 100644
index 0000000..c2235ab
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOpRasterizer.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <hwui/Bitmap.h>
+
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+
+#include "CanvasOps.h"
+
+#include <experimental/type_traits>
+#include <variant>
+
+namespace android::uirenderer {
+
+class CanvasOpBuffer;
+
+void rasterizeCanvasBuffer(const CanvasOpBuffer& source, SkCanvas* destination);
+
+class ImmediateModeRasterizer {
+public:
+    explicit ImmediateModeRasterizer(std::unique_ptr<SkCanvas>&& canvas) {
+        mCanvas = canvas.get();
+        mOwnership = std::move(canvas);
+    }
+
+    explicit ImmediateModeRasterizer(std::shared_ptr<SkCanvas> canvas) {
+        mCanvas = canvas.get();
+        mOwnership = std::move(canvas);
+    }
+
+    explicit ImmediateModeRasterizer(Bitmap& bitmap) {
+        mCanvas = &(mOwnership.emplace<SkCanvas>(bitmap.getSkBitmap()));
+    }
+
+    template <CanvasOpType T>
+    void draw(const CanvasOp<T>& op) {
+        if constexpr (CanvasOpTraits::can_draw<CanvasOp<T>>) {
+            op.draw(mCanvas);
+        }
+    }
+
+private:
+    SkCanvas* mCanvas;
+    // Just here to keep mCanvas alive. Thankfully we never need to actually look inside this...
+    std::variant<SkCanvas, std::shared_ptr<SkCanvas>, std::unique_ptr<SkCanvas>> mOwnership;
+};
+
+}  // namespace android::uirenderer
diff --git a/libs/hwui/canvas/CanvasOpTypes.h b/libs/hwui/canvas/CanvasOpTypes.h
new file mode 100644
index 0000000..2d4f2f5
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOpTypes.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <inttypes.h>
+
+namespace android::uirenderer {
+
+enum class CanvasOpType : int8_t {
+    // State ops
+    // TODO: Eliminate the end ops by having the start include the end-at position
+    Save,
+    SaveLayer,
+    SaveBehind,
+    Restore,
+    BeginZ,
+    EndZ,
+
+    // Clip ops
+    ClipRect,
+    ClipPath,
+
+    // Drawing ops
+    DrawColor,
+    DrawRect,
+
+    // TODO: Rest
+
+    COUNT  // must be last
+};
+
+}  // namespace android::uirenderer
\ No newline at end of file
diff --git a/libs/hwui/canvas/CanvasOps.h b/libs/hwui/canvas/CanvasOps.h
new file mode 100644
index 0000000..a31a91c
--- /dev/null
+++ b/libs/hwui/canvas/CanvasOps.h
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <SkAndroidFrameworkUtils.h>
+#include <SkCanvas.h>
+#include <SkPath.h>
+#include <log/log.h>
+
+#include "CanvasOpTypes.h"
+
+#include <experimental/type_traits>
+
+namespace android::uirenderer {
+
+template <CanvasOpType T>
+struct CanvasOp;
+
+struct CanvasOpTraits {
+    CanvasOpTraits() = delete;
+
+    template<class T>
+    using draw_t = decltype(std::integral_constant<void (T::*)(SkCanvas*) const, &T::draw>{});
+
+    template <class T>
+    static constexpr bool can_draw = std::experimental::is_detected_v<draw_t, T>;
+};
+
+#define ASSERT_DRAWABLE() private: constexpr void _check_drawable() \
+    { static_assert(CanvasOpTraits::can_draw<std::decay_t<decltype(*this)>>); }
+
+// ----------------------------------------------
+//   State Ops
+//  ---------------------------------------------
+
+template <>
+struct CanvasOp<CanvasOpType::Save> {
+    void draw(SkCanvas* canvas) const { canvas->save(); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::SaveLayer> {
+    SkCanvas::SaveLayerRec saveLayerRec;
+    void draw(SkCanvas* canvas) const { canvas->saveLayer(saveLayerRec); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::SaveBehind> {
+    SkRect bounds;
+    void draw(SkCanvas* canvas) const { SkAndroidFrameworkUtils::SaveBehind(canvas, &bounds); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::Restore> {
+    void draw(SkCanvas* canvas) const { canvas->restore(); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::BeginZ> {
+};
+template <>
+struct CanvasOp<CanvasOpType::EndZ> {};
+
+// ----------------------------------------------
+//   Clip Ops
+//  ---------------------------------------------
+
+template <>
+struct CanvasOp<CanvasOpType::ClipRect> {
+    SkRect rect;
+    SkClipOp clipOp;
+    void draw(SkCanvas* canvas) const { canvas->clipRect(rect, clipOp); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::ClipPath> {
+    SkPath path;
+    SkClipOp op;
+    void draw(SkCanvas* canvas) const { canvas->clipPath(path, op, true); }
+    ASSERT_DRAWABLE()
+};
+
+// ----------------------------------------------
+//   Drawing Ops
+//  ---------------------------------------------
+
+template <>
+struct CanvasOp<CanvasOpType::DrawColor> {
+    SkColor4f color;
+    SkBlendMode mode;
+    void draw(SkCanvas* canvas) const { canvas->drawColor(color, mode); }
+    ASSERT_DRAWABLE()
+};
+
+template <>
+struct CanvasOp<CanvasOpType::DrawRect> {
+    SkRect rect;
+    SkPaint paint;
+    void draw(SkCanvas* canvas) const { canvas->drawRect(rect, paint); }
+    ASSERT_DRAWABLE()
+};
+
+
+// cleanup our macros
+#undef ASSERT_DRAWABLE
+
+}  // namespace android::uirenderer
\ No newline at end of file
diff --git a/libs/hwui/canvas/OpBuffer.h b/libs/hwui/canvas/OpBuffer.h
new file mode 100644
index 0000000..398e090
--- /dev/null
+++ b/libs/hwui/canvas/OpBuffer.h
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <cinttypes>
+#include <cstddef>
+#include <cstdlib>
+#include <type_traits>
+#include <utility>
+
+namespace android::uirenderer {
+
+template <typename T>
+struct OpBufferItemHeader {
+    T type : 8;
+    uint32_t size : 24;
+};
+
+struct OpBufferAllocationHeader {
+    // Used size, including header size
+    size_t used = 0;
+    // Capacity, including header size
+    size_t capacity = 0;
+    // Offset relative to `this` at which the first item is
+    size_t startOffset = 0;
+    // Offset relative to `this` at which the last item is
+    size_t endOffset = 0;
+};
+
+#define BE_OPBUFFERS_FRIEND()                                      \
+    template <typename ItemTypes, template <ItemTypes> typename, typename, typename> \
+    friend class OpBuffer
+
+template <typename ItemTypes, template <ItemTypes> typename ItemContainer,
+          typename BufferHeader = OpBufferAllocationHeader,
+          typename ItemTypesSequence = std::make_index_sequence<static_cast<int>(ItemTypes::COUNT)>>
+class OpBuffer {
+    // Instead of re-aligning individual inserts, just pad the size of everything
+    // to a multiple of pointer alignment. This assumes we never work with doubles.
+    // Which we don't.
+    static constexpr size_t Alignment = alignof(void*);
+
+    static constexpr size_t PadAlign(size_t size) {
+        return (size + (Alignment - 1)) & -Alignment;
+    }
+
+    static constexpr auto STARTING_SIZE = PadAlign(sizeof(BufferHeader));
+
+public:
+    using ItemHeader = OpBufferItemHeader<ItemTypes>;
+
+    OpBuffer() = default;
+
+    // Prevent copying by default
+    OpBuffer(const OpBuffer&) = delete;
+    void operator=(const OpBuffer&) = delete;
+
+    OpBuffer(OpBuffer&& other) {
+        mBuffer = other.mBuffer;
+        other.mBuffer = nullptr;
+    }
+
+    void operator=(OpBuffer&& other) {
+        destroy();
+        mBuffer = other.mBuffer;
+        other.mBuffer = nullptr;
+    }
+
+    ~OpBuffer() {
+        destroy();
+    }
+
+    constexpr size_t capacity() const { return mBuffer ? mBuffer->capacity : 0; }
+
+    constexpr size_t size() const { return mBuffer ? mBuffer->used : 0; }
+
+    constexpr size_t remaining() const { return capacity() - size(); }
+
+    // TODO: Add less-copy'ing variants of this. emplace_back? deferred initialization?
+    template <ItemTypes T>
+    void push_container(ItemContainer<T>&& op) {
+        static_assert(alignof(ItemContainer<T>) <= Alignment);
+        static_assert(offsetof(ItemContainer<T>, header) == 0);
+
+        constexpr auto padded_size = PadAlign(sizeof(ItemContainer<T>));
+        if (remaining() < padded_size) {
+            resize(std::max(padded_size, capacity()) * 2);
+        }
+        mBuffer->endOffset = mBuffer->used;
+        mBuffer->used += padded_size;
+
+        void* allocateAt = reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->endOffset;
+        auto temp = new (allocateAt) ItemContainer<T>{std::move(op)};
+        temp->header = {.type = T, .size = padded_size};
+    }
+
+    void resize(size_t newsize) {
+        // Add the header size to newsize
+        const size_t adjustedSize = newsize + STARTING_SIZE;
+
+        if (adjustedSize < size()) {
+            // todo: throw?
+            return;
+        }
+        if (newsize == 0) {
+            free(mBuffer);
+            mBuffer = nullptr;
+        } else {
+            if (mBuffer) {
+                mBuffer = reinterpret_cast<BufferHeader*>(realloc(mBuffer, adjustedSize));
+                mBuffer->capacity = adjustedSize;
+            } else {
+                mBuffer = new (malloc(adjustedSize)) BufferHeader();
+                mBuffer->capacity = adjustedSize;
+                mBuffer->used = STARTING_SIZE;
+                mBuffer->startOffset = STARTING_SIZE;
+            }
+        }
+    }
+
+    template <typename F>
+    void for_each(F&& f) const {
+        for_each(std::forward<F>(f), ItemTypesSequence{});
+    }
+
+    void clear();
+
+    ItemHeader* first() const { return isEmpty() ? nullptr : itemAt(mBuffer->startOffset); }
+
+    ItemHeader* last() const { return isEmpty() ? nullptr : itemAt(mBuffer->endOffset); }
+
+private:
+    template <typename F, std::size_t... I>
+    void for_each(F&& f, std::index_sequence<I...>) const {
+        // Validate we're not empty
+        if (isEmpty()) return;
+
+        // Setup the jump table, mapping from each type to a springboard that invokes the template
+        // function with the appropriate concrete type
+        using F_PTR = decltype(&f);
+        using THUNK = void (*)(F_PTR, void*);
+        static constexpr auto jump = std::array<THUNK, sizeof...(I)>{[](F_PTR fp, void* t) {
+            (*fp)(reinterpret_cast<ItemContainer<static_cast<ItemTypes>(I)>*>(t));
+        }...};
+
+        // Do the actual iteration of each item
+        uint8_t* current = reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->startOffset;
+        uint8_t* end = reinterpret_cast<uint8_t*>(mBuffer) + mBuffer->used;
+        while (current != end) {
+            auto header = reinterpret_cast<ItemHeader*>(current);
+            // `f` could be a destructor, so ensure all accesses to the OP happen prior to invoking
+            // `f`
+            auto it = (void*)current;
+            current += header->size;
+            jump[static_cast<int>(header->type)](&f, it);
+        }
+    }
+
+    void destroy() {
+        clear();
+        resize(0);
+    }
+
+    bool offsetIsValid(size_t offset) const {
+        return offset >= mBuffer->startOffset && offset < mBuffer->used;
+    }
+
+    ItemHeader* itemAt(size_t offset) const {
+        if (!offsetIsValid(offset)) return nullptr;
+        return reinterpret_cast<ItemHeader*>(reinterpret_cast<uint8_t*>(mBuffer) + offset);
+    }
+
+    bool isEmpty() const { return mBuffer == nullptr || mBuffer->used == STARTING_SIZE; }
+
+    BufferHeader* mBuffer = nullptr;
+};
+
+template <typename ItemTypes, template <ItemTypes> typename ItemContainer, typename BufferHeader,
+        typename ItemTypeSequence>
+void OpBuffer<ItemTypes, ItemContainer, BufferHeader, ItemTypeSequence>::clear() {
+
+    // Don't need to do anything if we don't have a buffer
+    if (!mBuffer) return;
+
+    for_each([](auto op) {
+        using T = std::remove_reference_t<decltype(*op)>;
+        op->~T();
+    });
+    mBuffer->used = STARTING_SIZE;
+    mBuffer->startOffset = STARTING_SIZE;
+    mBuffer->endOffset = 0;
+}
+
+}  // namespace android::uirenderer
\ No newline at end of file
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index 6ece7ef..94a047c 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -100,6 +100,12 @@
 
     void getSkBitmap(SkBitmap* outBitmap);
 
+    SkBitmap getSkBitmap() {
+        SkBitmap ret;
+        getSkBitmap(&ret);
+        return ret;
+    }
+
     int getAshmemFd() const;
     size_t getAllocationByteCount() const;
 
diff --git a/libs/hwui/tests/common/CallCountingCanvas.h b/libs/hwui/tests/common/CallCountingCanvas.h
new file mode 100644
index 0000000..a965571
--- /dev/null
+++ b/libs/hwui/tests/common/CallCountingCanvas.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <SkCanvasVirtualEnforcer.h>
+#include <SkNoDrawCanvas.h>
+
+namespace android {
+namespace uirenderer {
+namespace test {
+
+class CallCountingCanvas final : public SkCanvasVirtualEnforcer<SkNoDrawCanvas> {
+private:
+    int START_MARKER;
+public:
+    CallCountingCanvas() : SkCanvasVirtualEnforcer<SkNoDrawCanvas>(1, 1) {}
+
+    int sumTotalDrawCalls() {
+        // Dirty hack assumes we're nothing but ints between START_MARKET and END_MARKER
+        int* cur = &START_MARKER + 1;
+        int* end = &END_MARKER;
+        int sum = 0;
+        while (cur != end) {
+            sum += *cur;
+            cur++;
+        }
+        return sum;
+    }
+
+    int drawPaintCount = 0;
+    void onDrawPaint(const SkPaint& paint) override {
+        drawPaintCount++;
+    }
+
+    int drawBehindCount = 0;
+    void onDrawBehind(const SkPaint&) override {
+        drawBehindCount++;
+    }
+
+    int drawRectCount = 0;
+    void onDrawRect(const SkRect& rect, const SkPaint& paint) override {
+        drawRectCount++;
+    }
+
+    int drawRRectCount = 0;
+    void onDrawRRect(const SkRRect& rrect, const SkPaint& paint) override {
+        drawRRectCount++;
+    }
+
+    int drawDRRectCount = 0;
+    void onDrawDRRect(const SkRRect& outer, const SkRRect& inner,
+                      const SkPaint& paint) override {
+        drawDRRectCount++;
+    }
+
+    int drawOvalCount = 0;
+    void onDrawOval(const SkRect& rect, const SkPaint& paint) override {
+        drawOvalCount++;
+    }
+
+    int drawArcCount = 0;
+    void onDrawArc(const SkRect& rect, SkScalar startAngle, SkScalar sweepAngle, bool useCenter,
+                   const SkPaint& paint) override {
+        drawArcCount++;
+    }
+
+    int drawPathCount = 0;
+    void onDrawPath(const SkPath& path, const SkPaint& paint) override {
+        drawPaintCount++;
+    }
+
+    int drawRegionCount = 0;
+    void onDrawRegion(const SkRegion& region, const SkPaint& paint) override {
+        drawRegionCount++;
+    }
+
+    int drawTextBlobCount = 0;
+    void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y,
+                        const SkPaint& paint) override {
+        drawTextBlobCount++;
+    }
+
+    int drawPatchCount = 0;
+    void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4],
+                     const SkPoint texCoords[4], SkBlendMode mode,
+                     const SkPaint& paint) override {
+        drawPatchCount++;
+    }
+
+    int drawPoints = 0;
+    void onDrawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[],
+                      const SkPaint& paint) override {
+        drawPoints++;
+    }
+
+    int drawImageCount = 0;
+    void onDrawImage(const SkImage* image, SkScalar dx, SkScalar dy,
+                     const SkPaint* paint) override {
+        drawImageCount++;
+    }
+
+    int drawImageRectCount = 0;
+    void onDrawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst,
+                         const SkPaint* paint, SkCanvas::SrcRectConstraint constraint) override {
+        drawImageRectCount++;
+    }
+
+    int drawImageNineCount = 0;
+    void onDrawImageNine(const SkImage* image, const SkIRect& center, const SkRect& dst,
+                         const SkPaint* paint) override {
+        drawImageNineCount++;
+    }
+
+    int drawImageLatticeCount = 0;
+    void onDrawImageLattice(const SkImage* image, const SkCanvas::Lattice& lattice,
+                            const SkRect& dst, const SkPaint* paint) override {
+        drawImageLatticeCount++;
+    }
+
+    int drawAtlasCount = 0;
+    void onDrawAtlas(const SkImage* atlas, const SkRSXform xform[], const SkRect rect[],
+                     const SkColor colors[], int count, SkBlendMode mode, const SkRect* cull,
+                     const SkPaint* paint) override {
+        drawAtlasCount++;
+    }
+
+    int drawAnnotationCount = 0;
+    void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value) override {
+        drawAnnotationCount++;
+    }
+
+    int drawShadowRecCount = 0;
+    void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override {
+        drawShadowRecCount++;
+    }
+
+    int drawDrawableCount = 0;
+    void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override {
+        drawDrawableCount++;
+    }
+
+    int drawPictureCount = 0;
+    void onDrawPicture(const SkPicture* picture, const SkMatrix* matrix,
+                       const SkPaint* paint) override {
+        drawPictureCount++;
+    }
+
+private:
+    int END_MARKER;
+};
+
+} /* namespace test */
+} /* namespace uirenderer */
+} /* namespace android */
\ No newline at end of file
diff --git a/libs/hwui/tests/unit/CanvasOpTests.cpp b/libs/hwui/tests/unit/CanvasOpTests.cpp
new file mode 100644
index 0000000..0815d15
--- /dev/null
+++ b/libs/hwui/tests/unit/CanvasOpTests.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2020 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 <canvas/CanvasOpBuffer.h>
+#include <canvas/CanvasOps.h>
+#include <canvas/CanvasOpRasterizer.h>
+
+#include <tests/common/CallCountingCanvas.h>
+
+using namespace android;
+using namespace android::uirenderer;
+using namespace android::uirenderer::test;
+
+// We lazy
+using Op = CanvasOpType;
+
+enum MockTypes {
+    Lifecycle,
+    COUNT
+};
+
+template<MockTypes T>
+struct MockOp;
+
+template<MockTypes T>
+struct MockOpContainer {
+    OpBufferItemHeader<MockTypes> header;
+    MockOp<T> impl;
+};
+
+struct LifecycleTracker {
+    int ctor_count = 0;
+    int dtor_count = 0;
+
+    int alive() { return ctor_count - dtor_count; }
+};
+
+template<>
+struct MockOp<MockTypes::Lifecycle> {
+    MockOp() = delete;
+    void operator=(const MockOp&) = delete;
+
+    MockOp(LifecycleTracker* tracker) : tracker(tracker) {
+        tracker->ctor_count += 1;
+    }
+
+    MockOp(const MockOp& other) {
+        tracker = other.tracker;
+        tracker->ctor_count += 1;
+    }
+
+    ~MockOp() {
+        tracker->dtor_count += 1;
+    }
+
+    LifecycleTracker* tracker = nullptr;
+};
+
+using MockBuffer = OpBuffer<MockTypes, MockOpContainer>;
+
+template<typename T>
+static int countItems(const T& t) {
+    int count = 0;
+    t.for_each([&](auto i) {
+        count++;
+    });
+    return count;
+}
+
+TEST(CanvasOp, lifecycleCheck) {
+    LifecycleTracker tracker;
+    {
+        MockBuffer buffer;
+        buffer.push_container(MockOpContainer<MockTypes::Lifecycle> {
+            .impl = MockOp<MockTypes::Lifecycle>{&tracker}
+        });
+        EXPECT_EQ(tracker.alive(), 1);
+        buffer.clear();
+        EXPECT_EQ(tracker.alive(), 0);
+    }
+    EXPECT_EQ(tracker.alive(), 0);
+}
+
+TEST(CanvasOp, lifecycleCheckMove) {
+    LifecycleTracker tracker;
+    {
+        MockBuffer buffer;
+        buffer.push_container(MockOpContainer<MockTypes::Lifecycle> {
+            .impl = MockOp<MockTypes::Lifecycle>{&tracker}
+        });
+        EXPECT_EQ(tracker.alive(), 1);
+        {
+            MockBuffer other(std::move(buffer));
+            EXPECT_EQ(tracker.alive(), 1);
+            EXPECT_EQ(buffer.size(), 0);
+            EXPECT_GT(other.size(), 0);
+            EXPECT_EQ(1, countItems(other));
+            EXPECT_EQ(0, countItems(buffer));
+
+            other.push_container(MockOpContainer<MockTypes::Lifecycle> {
+                .impl = MockOp<MockTypes::Lifecycle>{&tracker}
+            });
+
+            EXPECT_EQ(2, countItems(other));
+            EXPECT_EQ(2, tracker.alive());
+
+            buffer.push_container(MockOpContainer<MockTypes::Lifecycle> {
+                .impl = MockOp<MockTypes::Lifecycle>{&tracker}
+            });
+            EXPECT_EQ(1, countItems(buffer));
+            EXPECT_EQ(3, tracker.alive());
+
+            buffer = std::move(other);
+            EXPECT_EQ(2, countItems(buffer));
+            EXPECT_EQ(2, tracker.alive());
+        }
+        EXPECT_EQ(2, countItems(buffer));
+        EXPECT_EQ(2, tracker.alive());
+        buffer.clear();
+        EXPECT_EQ(0, countItems(buffer));
+        EXPECT_EQ(0, tracker.alive());
+    }
+    EXPECT_EQ(tracker.alive(), 0);
+}
+
+TEST(CanvasOp, simplePush) {
+    CanvasOpBuffer buffer;
+    EXPECT_EQ(buffer.size(), 0);
+    buffer.push<Op::Save>({});
+    buffer.push<Op::Save>({});
+    buffer.push<Op::Restore>({});
+    EXPECT_GT(buffer.size(), 0);
+
+    int saveCount = 0;
+    int restoreCount = 0;
+    int otherCount = 0;
+
+    buffer.for_each([&](auto op) {
+        switch (op->type()) {
+            case Op::Save:
+                saveCount++;
+                break;
+            case Op::Restore:
+                restoreCount++;
+                break;
+            default:
+                otherCount++;
+                break;
+        }
+    });
+
+    EXPECT_EQ(saveCount, 2);
+    EXPECT_EQ(restoreCount, 1);
+    EXPECT_EQ(otherCount, 0);
+
+    buffer.clear();
+    int itemCount = 0;
+    buffer.for_each([&](auto op) {
+        itemCount++;
+    });
+    EXPECT_EQ(itemCount, 0);
+    buffer.resize(0);
+    EXPECT_EQ(buffer.size(), 0);
+}
+
+TEST(CanvasOp, simpleDrawRect) {
+    CanvasOpBuffer buffer;
+    EXPECT_EQ(buffer.size(), 0);
+    buffer.push(CanvasOp<Op::DrawRect> {
+        .paint = SkPaint{},
+        .rect = SkRect::MakeEmpty()
+    });
+
+    CallCountingCanvas canvas;
+    EXPECT_EQ(0, canvas.sumTotalDrawCalls());
+    rasterizeCanvasBuffer(buffer, &canvas);
+    EXPECT_EQ(1, canvas.drawRectCount);
+    EXPECT_EQ(1, canvas.sumTotalDrawCalls());
+}
+
+TEST(CanvasOp, immediateRendering) {
+    auto canvas = std::make_shared<CallCountingCanvas>();
+
+    EXPECT_EQ(0, canvas->sumTotalDrawCalls());
+    ImmediateModeRasterizer rasterizer{canvas};
+    auto op = CanvasOp<Op::DrawRect> {
+        .paint = SkPaint{},
+        .rect = SkRect::MakeEmpty()
+    };
+    EXPECT_TRUE(CanvasOpTraits::can_draw<decltype(op)>);
+    rasterizer.draw(op);
+    EXPECT_EQ(1, canvas->drawRectCount);
+    EXPECT_EQ(1, canvas->sumTotalDrawCalls());
+}
\ No newline at end of file