Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5ea1724b authored by Chris Craik's avatar Chris Craik
Browse files

Rename OpReorderer to FrameReorderer

Also separate LayerReorderer into its own files.

Change-Id: Iafb6a156f760f62f831f6288fd0dadf1db25da24
parent 7435eb14
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -109,7 +109,8 @@ ifeq (true, $(HWUI_NEW_OPS))
        BakedOpDispatcher.cpp \
        BakedOpRenderer.cpp \
        BakedOpState.cpp \
        OpReorderer.cpp \
        FrameReorderer.cpp \
        LayerReorderer.cpp \
        RecordingCanvas.cpp

    hwui_cflags += -DHWUI_NEW_OPS
@@ -236,8 +237,8 @@ LOCAL_SRC_FILES += \
ifeq (true, $(HWUI_NEW_OPS))
    LOCAL_SRC_FILES += \
        tests/unit/BakedOpStateTests.cpp \
        tests/unit/RecordingCanvasTests.cpp \
        tests/unit/OpReordererTests.cpp
        tests/unit/FrameReordererTests.cpp \
        tests/unit/RecordingCanvasTests.cpp
endif

include $(BUILD_NATIVE_TEST)
@@ -298,7 +299,7 @@ LOCAL_SRC_FILES += \

ifeq (true, $(HWUI_NEW_OPS))
    LOCAL_SRC_FILES += \
        tests/microbench/OpReordererBench.cpp
        tests/microbench/FrameReordererBench.cpp
endif

include $(BUILD_EXECUTABLE)
+41 −377

File changed and moved.

Preview size limit exceeded, changes collapsed.

+24 −106
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 * Copyright (C) 2016 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.
@@ -14,12 +14,12 @@
 * limitations under the License.
 */

#ifndef ANDROID_HWUI_OP_REORDERER_H
#define ANDROID_HWUI_OP_REORDERER_H
#pragma once

#include "BakedOpState.h"
#include "CanvasState.h"
#include "DisplayList.h"
#include "LayerReorderer.h"
#include "RecordedOp.h"

#include <vector>
@@ -31,114 +31,34 @@ namespace android {
namespace uirenderer {

class BakedOpState;
class BatchBase;
class LayerUpdateQueue;
class MergingOpBatch;
class OffscreenBuffer;
class OpBatch;
class Rect;

typedef int batchid_t;
typedef const void* mergeid_t;

namespace OpBatchType {
    enum {
        Bitmap,
        MergedPatch,
        AlphaVertices,
        Vertices,
        AlphaMaskTexture,
        Text,
        ColorText,
        Shadow,
        TextureLayer,
        Functor,
        CopyToLayer,
        CopyFromLayer,

        Count // must be last
    };
}

class OpReorderer : public CanvasStateClient {
    typedef void (*BakedOpReceiver)(void*, const BakedOpState&);
    typedef void (*MergedOpReceiver)(void*, const MergedBakedOpList& opList);

/**
     * Stores the deferred render operations and state used to compute ordering
     * for a single FBO/layer.
     */
    class LayerReorderer {
    public:
        // Create LayerReorderer for Fbo0
        LayerReorderer(uint32_t width, uint32_t height, const Rect& repaintRect)
                : LayerReorderer(width, height, repaintRect, nullptr, nullptr) {};

        // Create LayerReorderer for an offscreen layer, where beginLayerOp is present for a
        // saveLayer, renderNode is present for a HW layer.
        LayerReorderer(uint32_t width, uint32_t height,
                const Rect& repaintRect, const BeginLayerOp* beginLayerOp, RenderNode* renderNode);

        // 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(LinearAllocator& allocator, 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(LinearAllocator& allocator,
                BakedOpState* op, batchid_t batchId, mergeid_t mergeId);

        void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers, MergedOpReceiver*) const;

        void deferLayerClear(const Rect& dstRect);

        bool empty() const {
            return mBatches.empty();
        }

        void clear() {
            mBatches.clear();
        }

        void dump() const;

        const uint32_t width;
        const uint32_t height;
        const Rect repaintRect;
        OffscreenBuffer* offscreenBuffer;
        const BeginLayerOp* beginLayerOp;
        const RenderNode* renderNode;
        const ClipRect viewportClip;

        // list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps
        std::vector<BakedOpState*> activeUnclippedSaveLayers;
    private:
        void flushLayerClears(LinearAllocator& allocator);

        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.
 * Traverses all of the drawing commands from the layers and RenderNodes passed into it, preparing
 * them to be rendered.
 *
 * Resolves final drawing state for each operation (including clip, alpha and matrix), and then
 * reorder and merge each op as it is resolved for drawing efficiency. Each layer of content (either
 * from the LayerUpdateQueue, or temporary layers created by saveLayer operations in the
 * draw stream) will create different reorder contexts, each in its own LayerReorderer.
 *
 * Then the prepared or 'baked' drawing commands can be issued by calling the templated
 * replayBakedOps() function, which will dispatch them (including any created merged op collections)
 * to a Dispatcher and Renderer. See BakedOpDispatcher for how these baked drawing operations are
 * resolved into Glops and rendered via BakedOpRenderer.
 *
 * This class is also the authoritative source for traversing RenderNodes, both for standard op
 * traversal within a DisplayList, and for out of order RenderNode traversal for Z and projection.
 */
        std::unordered_map<mergeid_t, MergingOpBatch*> mMergingBatchLookup[OpBatchType::Count];

        // Maps batch ids to the most recent *non-merging* batch of that id
        OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };

        std::vector<Rect> mClearRects;
    };

class FrameReorderer : public CanvasStateClient {
public:
    OpReorderer(const LayerUpdateQueue& layers, const SkRect& clip,
    FrameReorderer(const LayerUpdateQueue& layers, const SkRect& clip,
            uint32_t viewportWidth, uint32_t viewportHeight,
            const std::vector< sp<RenderNode> >& nodes, const Vector3& lightCenter);

    virtual ~OpReorderer() {}
    virtual ~FrameReorderer() {}

    /**
     * replayBakedOps() is templated based on what class will receive ops being replayed.
@@ -253,7 +173,7 @@ private:
            BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined);

    /**
     * Declares all OpReorderer::deferXXXXOp() methods for every RecordedOp type.
     * Declares all FrameReorderer::deferXXXXOp() methods for every RecordedOp type.
     *
     * These private methods are called from within deferImpl to defer each individual op
     * type differently.
@@ -287,5 +207,3 @@ private:

}; // namespace uirenderer
}; // namespace android

#endif // ANDROID_HWUI_OP_REORDERER_H
+367 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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 "LayerReorderer.h"

#include "BakedOpState.h"
#include "RenderNode.h"
#include "utils/PaintUtils.h"
#include "utils/TraceUtils.h"

#include <utils/TypeHelpers.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, id %d, merging %d, count %d, bounds " RECT_STRING,
                this, mBatchId, mMerging, mOps.size(), 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)
            , mClipSideFlags(op->computedState.clipSideFlags) {
    }

    /*
     * 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) {
        // Note: don't check color, since all currently mergeable ops can merge across colors
        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);

        // Because a new op must have passed canMergeWith(), we know it's passed the clipping compat
        // check, and doesn't extend past a side of the clip that's in use by the merged batch.
        // Therefore it's safe to simply always merge flags, and use the bounds as the clip rect.
        mClipSideFlags |= op->computedState.clipSideFlags;
    }

    int getClipSideFlags() const { return mClipSideFlags; }
    const Rect& getClipRect() const { return mBounds; }

private:
    int mClipSideFlags;
};

LayerReorderer::LayerReorderer(uint32_t width, uint32_t height,
        const Rect& repaintRect, const BeginLayerOp* beginLayerOp, RenderNode* renderNode)
        : width(width)
        , height(height)
        , repaintRect(repaintRect)
        , offscreenBuffer(renderNode ? renderNode->getLayer() : nullptr)
        , beginLayerOp(beginLayerOp)
        , renderNode(renderNode)
        , viewportClip(Rect(width, height)) {}

// 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 LayerReorderer::locateInsertIndex(int batchId, const Rect& clippedBounds,
        BatchBase** targetBatch, size_t* insertBatchIndex) const {
    for (int i = mBatches.size() - 1; i >= 0; 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 LayerReorderer::deferLayerClear(const Rect& rect) {
    mClearRects.push_back(rect);
}

void LayerReorderer::flushLayerClears(LinearAllocator& allocator) {
    if (CC_UNLIKELY(!mClearRects.empty())) {
        const int vertCount = mClearRects.size() * 4;
        // put the verts in the frame allocator, since
        //     1) SimpleRectsOps needs verts, not rects
        //     2) even if mClearRects stored verts, std::vectors will move their contents
        Vertex* const verts = (Vertex*) allocator.alloc(vertCount * sizeof(Vertex));

        Vertex* currentVert = verts;
        Rect bounds = mClearRects[0];
        for (auto&& rect : mClearRects) {
            bounds.unionWith(rect);
            Vertex::set(currentVert++, rect.left, rect.top);
            Vertex::set(currentVert++, rect.right, rect.top);
            Vertex::set(currentVert++, rect.left, rect.bottom);
            Vertex::set(currentVert++, rect.right, rect.bottom);
        }
        mClearRects.clear(); // discard rects before drawing so this method isn't reentrant

        // One or more unclipped saveLayers have been enqueued, with deferred clears.
        // Flush all of these clears with a single draw
        SkPaint* paint = allocator.create<SkPaint>();
        paint->setXfermodeMode(SkXfermode::kClear_Mode);
        SimpleRectsOp* op = new (allocator) SimpleRectsOp(bounds,
                Matrix4::identity(), nullptr, paint,
                verts, vertCount);
        BakedOpState* bakedState = BakedOpState::directConstruct(allocator,
                &viewportClip, bounds, *op);


        deferUnmergeableOp(allocator, bakedState, OpBatchType::Vertices);
    }
}

void LayerReorderer::deferUnmergeableOp(LinearAllocator& allocator,
        BakedOpState* op, batchid_t batchId) {
    if (batchId != OpBatchType::CopyToLayer) {
        // if first op after one or more unclipped saveLayers, flush the layer clears
        flushLayerClears(allocator);
    }

    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 (allocator) OpBatch(batchId, op);
        mBatchLookup[batchId] = targetBatch;
        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
    }
}

void LayerReorderer::deferMergeableOp(LinearAllocator& allocator,
        BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
    if (batchId != OpBatchType::CopyToLayer) {
        // if first op after one or more unclipped saveLayers, flush the layer clears
        flushLayerClears(allocator);
    }
    MergingOpBatch* targetBatch = nullptr;

    // Try to merge with any existing batch with same mergeId
    auto getResult = mMergingBatchLookup[batchId].find(mergeId);
    if (getResult != mMergingBatchLookup[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 (allocator) MergingOpBatch(batchId, op);
        mMergingBatchLookup[batchId].insert(std::make_pair(mergeId, targetBatch));

        mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch);
    }
}

void LayerReorderer::replayBakedOpsImpl(void* arg,
        BakedOpReceiver* unmergedReceivers, MergedOpReceiver* mergedReceivers) const {
    ATRACE_NAME("flush drawing commands");
    for (const BatchBase* batch : mBatches) {
        size_t size = batch->getOps().size();
        if (size > 1 && batch->isMerging()) {
            int opId = batch->getOps()[0]->op->opId;
            const MergingOpBatch* mergingBatch = static_cast<const MergingOpBatch*>(batch);
            MergedBakedOpList data = {
                    batch->getOps().data(),
                    size,
                    mergingBatch->getClipSideFlags(),
                    mergingBatch->getClipRect()
            };
            mergedReceivers[opId](arg, data);
        } else {
            for (const BakedOpState* op : batch->getOps()) {
                unmergedReceivers[op->op->opId](arg, *op);
            }
        }
    }
}

void LayerReorderer::dump() const {
    ALOGD("LayerReorderer %p, %ux%u buffer %p, blo %p, rn %p",
            this, width, height, offscreenBuffer, beginLayerOp, renderNode);
    for (const BatchBase* batch : mBatches) {
        batch->dump();
    }
}

} // namespace uirenderer
} // namespace android
+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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 "ClipArea.h"
#include "Rect.h"

#include <vector>
#include <unordered_map>

struct SkRect;

namespace android {
namespace uirenderer {

class BakedOpState;
struct BeginLayerOp;
class BatchBase;
class LinearAllocator;
struct MergedBakedOpList;
class MergingOpBatch;
class OffscreenBuffer;
class OpBatch;
class RenderNode;

typedef int batchid_t;
typedef const void* mergeid_t;

namespace OpBatchType {
    enum {
        Bitmap,
        MergedPatch,
        AlphaVertices,
        Vertices,
        AlphaMaskTexture,
        Text,
        ColorText,
        Shadow,
        TextureLayer,
        Functor,
        CopyToLayer,
        CopyFromLayer,

        Count // must be last
    };
}

typedef void (*BakedOpReceiver)(void*, const BakedOpState&);
typedef void (*MergedOpReceiver)(void*, const MergedBakedOpList& opList);

/**
 * Stores the deferred render operations and state used to compute ordering
 * for a single FBO/layer.
 */
class LayerReorderer {
public:
    // Create LayerReorderer for Fbo0
    LayerReorderer(uint32_t width, uint32_t height, const Rect& repaintRect)
            : LayerReorderer(width, height, repaintRect, nullptr, nullptr) {};

    // Create LayerReorderer for an offscreen layer, where beginLayerOp is present for a
    // saveLayer, renderNode is present for a HW layer.
    LayerReorderer(uint32_t width, uint32_t height,
            const Rect& repaintRect, const BeginLayerOp* beginLayerOp, RenderNode* renderNode);

    // 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(LinearAllocator& allocator, 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(LinearAllocator& allocator,
            BakedOpState* op, batchid_t batchId, mergeid_t mergeId);

    void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers, MergedOpReceiver*) const;

    void deferLayerClear(const Rect& dstRect);

    bool empty() const {
        return mBatches.empty();
    }

    void clear() {
        mBatches.clear();
    }

    void dump() const;

    const uint32_t width;
    const uint32_t height;
    const Rect repaintRect;
    OffscreenBuffer* offscreenBuffer;
    const BeginLayerOp* beginLayerOp;
    const RenderNode* renderNode;
    const ClipRect viewportClip;

    // list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps
    std::vector<BakedOpState*> activeUnclippedSaveLayers;
private:
    void flushLayerClears(LinearAllocator& allocator);

    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*> mMergingBatchLookup[OpBatchType::Count];

    // Maps batch ids to the most recent *non-merging* batch of that id
    OpBatch* mBatchLookup[OpBatchType::Count] = { nullptr };

    std::vector<Rect> mClearRects;
};

}; // namespace uirenderer
}; // namespace android
Loading