diff options
-rw-r--r-- | libs/hwui/BakedOpRenderer.cpp | 35 | ||||
-rw-r--r-- | libs/hwui/BakedOpRenderer.h | 2 | ||||
-rw-r--r-- | libs/hwui/BakedOpState.h | 2 | ||||
-rw-r--r-- | libs/hwui/CanvasState.cpp | 4 | ||||
-rw-r--r-- | libs/hwui/CanvasState.h | 2 | ||||
-rw-r--r-- | libs/hwui/OpReorderer.cpp | 251 | ||||
-rw-r--r-- | libs/hwui/OpReorderer.h | 119 | ||||
-rw-r--r-- | libs/hwui/OpenGLRenderer.cpp | 20 | ||||
-rw-r--r-- | libs/hwui/RecordedOp.h | 30 | ||||
-rw-r--r-- | libs/hwui/RecordingCanvas.cpp | 76 | ||||
-rw-r--r-- | libs/hwui/RecordingCanvas.h | 4 | ||||
-rw-r--r-- | libs/hwui/RenderNode.cpp | 2 | ||||
-rw-r--r-- | libs/hwui/RenderProperties.h | 4 | ||||
-rw-r--r-- | libs/hwui/Snapshot.h | 3 | ||||
-rw-r--r-- | libs/hwui/microbench/OpReordererBench.cpp | 2 | ||||
-rw-r--r-- | libs/hwui/renderthread/CanvasContext.cpp | 2 | ||||
-rw-r--r-- | libs/hwui/unit_tests/OpReordererTests.cpp | 222 | ||||
-rw-r--r-- | libs/hwui/unit_tests/RecordingCanvasTests.cpp | 122 | ||||
-rw-r--r-- | libs/hwui/unit_tests/TestUtils.h | 6 |
19 files changed, 694 insertions, 214 deletions
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp index 4d9f9b479343..94806cab2fc5 100644 --- a/libs/hwui/BakedOpRenderer.cpp +++ b/libs/hwui/BakedOpRenderer.cpp @@ -74,53 +74,64 @@ void BakedOpRenderer::endFrame(Info& info) { #endif } -void BakedOpRenderer::onRenderNodeOp(Info*, const RenderNodeOp&, const BakedOpState&) { +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); +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) + 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); + info.renderGlop(state, glop); } -void BakedOpRenderer::onRectOp(Info* info, const RectOp& op, const BakedOpState& state) { +void BakedOpRenderer::onRectOp(Info& info, const RectOp& op, const BakedOpState& state) { Glop glop; - GlopBuilder(info->renderState, info->caches, &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); + info.renderGlop(state, glop); } -void BakedOpRenderer::onSimpleRectsOp(Info* info, const SimpleRectsOp& op, const BakedOpState& state) { +void BakedOpRenderer::onSimpleRectsOp(Info& info, const SimpleRectsOp& op, const BakedOpState& state) { Glop glop; - GlopBuilder(info->renderState, info->caches, &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); + info.renderGlop(state, glop); } +void BakedOpRenderer::onBeginLayerOp(Info& info, const BeginLayerOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("unsupported operation"); +} + +void BakedOpRenderer::onEndLayerOp(Info& info, const EndLayerOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("unsupported operation"); +} + +void BakedOpRenderer::onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) { + LOG_ALWAYS_FATAL("unsupported operation"); +} } // namespace uirenderer } // namespace android diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h index b8b4426412e4..f45dbe41f308 100644 --- a/libs/hwui/BakedOpRenderer.h +++ b/libs/hwui/BakedOpRenderer.h @@ -65,7 +65,7 @@ public: * 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); + #define BAKED_OP_RENDERER_METHOD(Type) static void on##Type(Info& info, const Type& op, const BakedOpState& state); MAP_OPS(BAKED_OP_RENDERER_METHOD); }; diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h index e2201ca06a4b..ddb8c84e08f4 100644 --- a/libs/hwui/BakedOpState.h +++ b/libs/hwui/BakedOpState.h @@ -68,7 +68,7 @@ public: // resolvedClipRect = intersect(parentMatrix * localClip, parentClip) clipRect = recordedOp.localClipRect; snapshot.transform->mapRect(clipRect); - clipRect.doIntersect(snapshot.getClipRect()); + clipRect.doIntersect(snapshot.getRenderTargetClip()); clipRect.snapToPixelBoundaries(); // resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect) diff --git a/libs/hwui/CanvasState.cpp b/libs/hwui/CanvasState.cpp index eca71c6e0e8d..6a6cc42043df 100644 --- a/libs/hwui/CanvasState.cpp +++ b/libs/hwui/CanvasState.cpp @@ -259,7 +259,7 @@ bool CanvasState::calculateQuickRejectForScissor(float left, float top, currentTransform()->mapRect(r); r.snapGeometryToPixelBoundaries(snapOut); - Rect clipRect(currentClipRect()); + Rect clipRect(currentRenderTargetClip()); clipRect.snapToPixelBoundaries(); if (!clipRect.intersects(r)) return true; @@ -287,7 +287,7 @@ bool CanvasState::quickRejectConservative(float left, float top, currentTransform()->mapRect(r); r.roundOut(); // rounded out to be conservative - Rect clipRect(currentClipRect()); + Rect clipRect(currentRenderTargetClip()); clipRect.snapToPixelBoundaries(); if (!clipRect.intersects(r)) return true; diff --git a/libs/hwui/CanvasState.h b/libs/hwui/CanvasState.h index be57f44210ef..4709ef41915f 100644 --- a/libs/hwui/CanvasState.h +++ b/libs/hwui/CanvasState.h @@ -147,7 +147,7 @@ public: void setInvisible(bool value) { mSnapshot->invisible = value; } inline const mat4* currentTransform() const { return currentSnapshot()->transform; } - inline const Rect& currentClipRect() const { return currentSnapshot()->getClipRect(); } + inline const Rect& currentRenderTargetClip() const { return currentSnapshot()->getRenderTargetClip(); } inline Region* currentRegion() const { return currentSnapshot()->region; } inline int currentFlags() const { return currentSnapshot()->flags; } const Vector3& currentLightCenter() const { return currentSnapshot()->getRelativeLightCenter(); } diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp index 7c0e2570972a..c1417c451895 100644 --- a/libs/hwui/OpReorderer.cpp +++ b/libs/hwui/OpReorderer.cpp @@ -52,7 +52,8 @@ public: const std::vector<BakedOpState*>& getOps() const { return mOps; } void dump() const { - ALOGD(" Batch %p, merging %d, bounds " RECT_STRING, this, mMerging, RECT_ARGS(mBounds)); + ALOGD(" Batch %p, id %d, merging %d, count %d, bounds " RECT_STRING, + this, mBatchId, mMerging, mOps.size(), RECT_ARGS(mBounds)); } protected: batchid_t mBatchId; @@ -201,17 +202,106 @@ private: 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; +// 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::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 OpReorderer::LayerReorderer::deferUnmergeableOp(LinearAllocator& allocator, + 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 (allocator) 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::LayerReorderer::deferMergeableOp(LinearAllocator& allocator, + BakedOpState* op, batchid_t batchId, mergeid_t mergeId) { + 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 OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers) const { + 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); + } + } +} + +void OpReorderer::LayerReorderer::dump() const { + for (const BatchBase* batch : mBatches) { + batch->dump(); + } +} OpReorderer::OpReorderer() - : mCanvasState(sNullClient) { + : mCanvasState(*this) { + mLayerReorderers.emplace_back(); + mLayerStack.push_back(0); } +void OpReorderer::onViewportInitialized() {} + +void OpReorderer::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) {} + void OpReorderer::defer(const SkRect& clip, int viewportWidth, int viewportHeight, const std::vector< sp<RenderNode> >& nodes) { mCanvasState.initializeSaveStack(viewportWidth, viewportHeight, @@ -244,11 +334,11 @@ void OpReorderer::defer(int viewportWidth, int viewportHeight, const DisplayList * 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) \ +#define OP_RECEIVER(Type) \ [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); }, void OpReorderer::deferImpl(const DisplayList& displayList) { static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = { - MAP_OPS(OP_RECIEVER) + MAP_OPS(OP_RECEIVER) }; for (const DisplayList::Chunk& chunk : displayList.getChunks()) { for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) { @@ -260,23 +350,18 @@ void OpReorderer::deferImpl(const DisplayList& displayList) { 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); - } + // Relay through layers in reverse order, since layers + // later in the list will be drawn by earlier ones + for (int i = mLayerReorderers.size() - 1; i >= 0; i--) { + mLayerReorderers[i].replayBakedOpsImpl(arg, receivers); } } -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); + int count = mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag); // apply state from RecordedOp mCanvasState.concatMatrix(op.localMatrix); @@ -285,10 +370,10 @@ void OpReorderer::onRenderNodeOp(const RenderNodeOp& op) { // apply RenderProperties state if (op.renderNode->applyViewProperties(mCanvasState)) { - // not rejected do ops... + // if node not rejected based on properties, do ops... deferImpl(op.renderNode->getDisplayList()); } - mCanvasState.restore(); + mCanvasState.restoreToCount(count); } static batchid_t tessellatedBatchId(const SkPaint& paint) { @@ -298,104 +383,70 @@ static batchid_t tessellatedBatchId(const SkPaint& paint) { } void OpReorderer::onBitmapOp(const BitmapOp& op) { - BakedOpState* bakedStateOp = bakeOpState(op); + BakedOpState* bakedStateOp = tryBakeOpState(op); if (!bakedStateOp) return; // quick rejected mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID(); // TODO: AssetAtlas - - deferMergeableOp(bakedStateOp, OpBatchType::Bitmap, mergeId); + currentLayer().deferMergeableOp(mAllocator, bakedStateOp, OpBatchType::Bitmap, mergeId); } void OpReorderer::onRectOp(const RectOp& op) { - BakedOpState* bakedStateOp = bakeOpState(op); + BakedOpState* bakedStateOp = tryBakeOpState(op); if (!bakedStateOp) return; // quick rejected - deferUnmergeableOp(bakedStateOp, tessellatedBatchId(*op.paint)); + currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, tessellatedBatchId(*op.paint)); } void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) { - BakedOpState* bakedStateOp = bakeOpState(op); + BakedOpState* bakedStateOp = tryBakeOpState(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 (int 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; - } - } + currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices); } -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); - } +// TODO: test rejection at defer time, where the bounds become empty +void OpReorderer::onBeginLayerOp(const BeginLayerOp& op) { + mCanvasState.save(SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag); + mCanvasState.writableSnapshot()->transform->loadIdentity(); + mCanvasState.writableSnapshot()->initializeViewport( + (int) op.unmappedBounds.getWidth(), (int) op.unmappedBounds.getHeight()); + mCanvasState.writableSnapshot()->roundRectClipState = nullptr; + + // create a new layer, and push its index on the stack + mLayerStack.push_back(mLayerReorderers.size()); + mLayerReorderers.emplace_back(); + mLayerReorderers.back().beginLayerOp = &op; } -// 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)); +void OpReorderer::onEndLayerOp(const EndLayerOp& /* ignored */) { + mCanvasState.restore(); - mBatches.insert(mBatches.begin() + insertBatchIndex, targetBatch); + const BeginLayerOp& beginLayerOp = *currentLayer().beginLayerOp; + + // pop finished layer off of the stack + int finishedLayerIndex = mLayerStack.back(); + mLayerStack.pop_back(); + + // record the draw operation into the previous layer's list of draw commands + // uses state from the associated beginLayerOp, since it has all the state needed for drawing + LayerOp* drawLayerOp = new (mAllocator) LayerOp( + beginLayerOp.unmappedBounds, + beginLayerOp.localMatrix, + beginLayerOp.localClipRect, + beginLayerOp.paint); + BakedOpState* bakedOpState = tryBakeOpState(*drawLayerOp); + + if (bakedOpState) { + // Layer will be drawn into parent layer (which is now current, since we popped mLayerStack) + currentLayer().deferUnmergeableOp(mAllocator, bakedOpState, OpBatchType::Bitmap); + } else { + // Layer won't be drawn - delete its drawing batches to prevent it from doing any work + mLayerReorderers[finishedLayerIndex].clear(); + return; } } -void OpReorderer::dump() { - for (const BatchBase* batch : mBatches) { - batch->dump(); - } +void OpReorderer::onLayerOp(const LayerOp& op) { + LOG_ALWAYS_FATAL("unsupported"); } } // namespace uirenderer diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h index 6776a3c9fc18..73dc9af01f71 100644 --- a/libs/hwui/OpReorderer.h +++ b/libs/hwui/OpReorderer.h @@ -54,19 +54,63 @@ namespace OpBatchType { }; } -class OpReorderer { +class OpReorderer : public CanvasStateClient { + typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpReceiver; + + /** + * Stores the deferred render operations and state used to compute ordering + * for a single FBO/layer. + */ + class LayerReorderer { + public: + // 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) const; + + void clear() { + mBatches.clear(); + } + + void dump() const; + + const BeginLayerOp* beginLayerOp = nullptr; + + private: + 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 }; + + }; public: OpReorderer(); + virtual ~OpReorderer() {} // TODO: not final, just presented this way for simplicity. Layers too? void defer(const SkRect& clip, int viewportWidth, int viewportHeight, const std::vector< sp<RenderNode> >& nodes); void defer(int viewportWidth, int viewportHeight, const DisplayList& displayList); - typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpReceiver; /** - * replayBakedOps() is templated based on what class will recieve ops being replayed. + * replayBakedOps() is templated based on what class will receive 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. @@ -77,19 +121,37 @@ public: */ #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); \ + StaticReceiver::on##Type(*(static_cast<Arg*>(internalArg)), static_cast<const Type&>(op), state); \ }, template <typename StaticReceiver, typename Arg> - void replayBakedOps(Arg* arg) { + void replayBakedOps(Arg& arg) { static BakedOpReceiver receivers[] = { MAP_OPS(BAKED_OP_RECEIVER) }; - StaticReceiver::startFrame(*arg); - replayBakedOpsImpl((void*)arg, receivers); - StaticReceiver::endFrame(*arg); + StaticReceiver::startFrame(arg); + replayBakedOpsImpl((void*)&arg, receivers); + StaticReceiver::endFrame(arg); + } + + void dump() const { + for (auto&& layer : mLayerReorderers) { + layer.dump(); + } } + + /////////////////////////////////////////////////////////////////// + /// CanvasStateClient interface + /////////////////////////////////////////////////////////////////// + virtual void onViewportInitialized() override; + virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override; + virtual GLuint getTargetFbo() const override { return 0; } + private: - BakedOpState* bakeOpState(const RecordedOp& recordedOp); + LayerReorderer& currentLayer() { return mLayerReorderers[mLayerStack.back()]; } + + BakedOpState* tryBakeOpState(const RecordedOp& recordedOp) { + return BakedOpState::tryConstruct(mAllocator, *mCanvasState.currentSnapshot(), recordedOp); + } void deferImpl(const DisplayList& displayList); @@ -105,36 +167,27 @@ private: 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); + // List of every deferred layer's render state. Replayed in reverse order to render a frame. + std::vector<LayerReorderer> mLayerReorderers; - // 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]; + /* + * Stack of indices within mLayerReorderers representing currently active layers. If drawing + * layerA within a layerB, will contain, in order: + * - 0 (representing FBO 0, always present) + * - layerB's index + * - layerA's index + * + * Note that this doesn't vector doesn't always map onto all values of mLayerReorderers. When a + * layer is finished deferring, it will still be represented in mLayerReorderers, but it's index + * won't be in mLayerStack. This is because it can be replayed, but can't have any more drawing + * ops added to it. + */ + std::vector<size_t> mLayerStack; - // 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; - - int mEarliestBatchIndex = 0; }; }; // namespace uirenderer diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp index cd03ac407d81..d4f65b635d4c 100644 --- a/libs/hwui/OpenGLRenderer.cpp +++ b/libs/hwui/OpenGLRenderer.cpp @@ -223,7 +223,7 @@ void OpenGLRenderer::resumeAfterLayer() { void OpenGLRenderer::callDrawGLFunction(Functor* functor, Rect& dirty) { if (mState.currentlyIgnored()) return; - Rect clip(mState.currentClipRect()); + Rect clip(mState.currentRenderTargetClip()); clip.snapToPixelBoundaries(); // Since we don't know what the functor will draw, let's dirty @@ -488,7 +488,7 @@ void OpenGLRenderer::calculateLayerBoundsAndClip(Rect& bounds, Rect& clip, bool currentTransform()->mapRect(bounds); // Layers only make sense if they are in the framebuffer's bounds - bounds.doIntersect(mState.currentClipRect()); + bounds.doIntersect(mState.currentRenderTargetClip()); if (!bounds.isEmpty()) { // We cannot work with sub-pixels in this case bounds.snapToPixelBoundaries(); @@ -1036,7 +1036,7 @@ void OpenGLRenderer::dirtyLayer(const float left, const float top, } void OpenGLRenderer::dirtyLayerUnchecked(Rect& bounds, Region* region) { - bounds.doIntersect(mState.currentClipRect()); + bounds.doIntersect(mState.currentRenderTargetClip()); if (!bounds.isEmpty()) { bounds.snapToPixelBoundaries(); android::Rect dirty(bounds.left, bounds.top, bounds.right, bounds.bottom); @@ -1084,7 +1084,7 @@ void OpenGLRenderer::clearLayerRegions() { .setMeshIndexedQuads(&mesh[0], quadCount) .setFillClear() .setTransform(*currentSnapshot(), transformFlags) - .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getClipRect())) + .setModelViewOffsetRect(0, 0, Rect(currentSnapshot()->getRenderTargetClip())) .build(); renderGlop(glop, GlopRenderType::LayerClear); @@ -1099,7 +1099,7 @@ void OpenGLRenderer::clearLayerRegions() { /////////////////////////////////////////////////////////////////////////////// bool OpenGLRenderer::storeDisplayState(DeferredDisplayState& state, int stateDeferFlags) { - const Rect& currentClip = mState.currentClipRect(); + const Rect& currentClip = mState.currentRenderTargetClip(); const mat4* currentMatrix = currentTransform(); if (stateDeferFlags & kStateDeferFlag_Draw) { @@ -1187,7 +1187,7 @@ void OpenGLRenderer::setupMergedMultiDraw(const Rect* clipRect) { /////////////////////////////////////////////////////////////////////////////// void OpenGLRenderer::setScissorFromClip() { - Rect clip(mState.currentClipRect()); + Rect clip(mState.currentRenderTargetClip()); clip.snapToPixelBoundaries(); if (mRenderState.scissor().set(clip.left, getViewportHeight() - clip.bottom, @@ -1430,7 +1430,7 @@ void OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t return; } - DeferredDisplayList deferredList(mState.currentClipRect()); + DeferredDisplayList deferredList(mState.currentRenderTargetClip()); DeferStateStruct deferStruct(deferredList, *this, replayFlags); renderNode->defer(deferStruct, 0); @@ -1765,7 +1765,7 @@ void OpenGLRenderer::drawColor(int color, SkXfermode::Mode mode) { // No need to check against the clip, we fill the clip region if (mState.currentlyIgnored()) return; - Rect clip(mState.currentClipRect()); + Rect clip(mState.currentRenderTargetClip()); clip.snapToPixelBoundaries(); SkPaint paint; @@ -2030,7 +2030,7 @@ void OpenGLRenderer::drawPosText(const char* text, int bytesCount, int count, } fontRenderer.setTextureFiltering(linearFilter); - const Rect& clip(pureTranslate ? writableSnapshot()->getClipRect() : writableSnapshot()->getLocalClip()); + const Rect& clip(pureTranslate ? writableSnapshot()->getRenderTargetClip() : writableSnapshot()->getLocalClip()); Rect bounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f); TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint); @@ -2191,7 +2191,7 @@ void OpenGLRenderer::drawText(const char* text, int bytesCount, int count, float fontRenderer.setTextureFiltering(linearFilter); // TODO: Implement better clipping for scaled/rotated text - const Rect* clip = !pureTranslate ? nullptr : &mState.currentClipRect(); + const Rect* clip = !pureTranslate ? nullptr : &mState.currentRenderTargetClip(); Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f); bool status; diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h index a69f0308ebc5..dd016372584a 100644 --- a/libs/hwui/RecordedOp.h +++ b/libs/hwui/RecordedOp.h @@ -41,7 +41,10 @@ struct Vertex; OP_FN(BitmapOp) \ OP_FN(RectOp) \ OP_FN(RenderNodeOp) \ - OP_FN(SimpleRectsOp) + OP_FN(SimpleRectsOp) \ + OP_FN(BeginLayerOp) \ + OP_FN(EndLayerOp) \ + OP_FN(LayerOp) // Generate OpId enum #define IDENTITY_FN(Type) Type, @@ -112,6 +115,31 @@ struct SimpleRectsOp : RecordedOp { // Filled, no AA (TODO: better name?) const size_t vertexCount; }; +/** + * Stateful operation! denotes the creation of an off-screen layer, + * and that commands following will render into it. + */ +struct BeginLayerOp : RecordedOp { + BeginLayerOp(BASE_PARAMS) + : SUPER(BeginLayerOp) {} +}; + +/** + * Stateful operation! Denotes end of off-screen layer, and that + * commands since last BeginLayerOp should be drawn into parent FBO. + * + * State in this op is empty, it just serves to signal that a layer has been finished. + */ +struct EndLayerOp : RecordedOp { + EndLayerOp() + : RecordedOp(RecordedOpId::EndLayerOp, Rect(0, 0), Matrix4::identity(), Rect(0, 0), nullptr) {} +}; + +struct LayerOp : RecordedOp { + LayerOp(BASE_PARAMS) + : SUPER(LayerOp) {} +}; + }; // namespace uirenderer }; // namespace android diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 3b413aaec0e0..1f113bc19ebb 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -73,6 +73,20 @@ SkCanvas* RecordingCanvas::asSkCanvas() { } // ---------------------------------------------------------------------------- +// CanvasStateClient implementation +// ---------------------------------------------------------------------------- + +void RecordingCanvas::onViewportInitialized() { + +} + +void RecordingCanvas::onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) { + if (removed.flags & Snapshot::kFlagIsFboLayer) { + addOp(new (alloc()) EndLayerOp()); + } +} + +// ---------------------------------------------------------------------------- // android/graphics/Canvas state operations // ---------------------------------------------------------------------------- // Save (layer) @@ -97,8 +111,66 @@ void RecordingCanvas::restoreToCount(int saveCount) { int RecordingCanvas::saveLayer(float left, float top, float right, float bottom, const SkPaint* paint, SkCanvas::SaveFlags flags) { - LOG_ALWAYS_FATAL("TODO"); - return 0; + if (!(flags & SkCanvas::kClipToLayer_SaveFlag)) { + LOG_ALWAYS_FATAL("unclipped layers not supported"); + } + // force matrix/clip isolation for layer + flags |= SkCanvas::kClip_SaveFlag | SkCanvas::kMatrix_SaveFlag; + + + const Snapshot& previous = *mState.currentSnapshot(); + + // initialize the snapshot as though it almost represents an FBO layer so deferred draw + // operations will be able to store and restore the current clip and transform info, and + // quick rejection will be correct (for display lists) + + const Rect untransformedBounds(left, top, right, bottom); + + // determine clipped bounds relative to previous viewport. + Rect visibleBounds = untransformedBounds; + previous.transform->mapRect(visibleBounds); + + + visibleBounds.doIntersect(previous.getRenderTargetClip()); + visibleBounds.snapToPixelBoundaries(); + + Rect previousViewport(0, 0, previous.getViewportWidth(), previous.getViewportHeight()); + visibleBounds.doIntersect(previousViewport); + + // Map visible bounds back to layer space, and intersect with parameter bounds + Rect layerBounds = visibleBounds; + Matrix4 inverse; + inverse.loadInverse(*previous.transform); + inverse.mapRect(layerBounds); + layerBounds.doIntersect(untransformedBounds); + + int saveValue = mState.save((int) flags); + Snapshot& snapshot = *mState.writableSnapshot(); + + // layerBounds is now original bounds, but with clipped to clip + // and viewport to ensure it's minimal size. + if (layerBounds.isEmpty() || untransformedBounds.isEmpty()) { + // Don't bother recording layer, since it's been rejected + snapshot.resetClip(0, 0, 0, 0); + return saveValue; + } + + snapshot.flags |= Snapshot::kFlagFboTarget | Snapshot::kFlagIsFboLayer; + snapshot.initializeViewport(untransformedBounds.getWidth(), untransformedBounds.getHeight()); + snapshot.resetTransform(-untransformedBounds.left, -untransformedBounds.top, 0.0f); + + Rect clip = layerBounds; + clip.translate(-untransformedBounds.left, -untransformedBounds.top); + snapshot.resetClip(clip.left, clip.top, clip.right, clip.bottom); + snapshot.roundRectClipState = nullptr; + + addOp(new (alloc()) BeginLayerOp( + Rect(left, top, right, bottom), + *previous.transform, // transform to *draw* with + previous.getRenderTargetClip(), // clip to *draw* with + refPaint(paint))); + + return saveValue; } // Matrix diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h index 2179e4c24580..9c32b1a769dc 100644 --- a/libs/hwui/RecordingCanvas.h +++ b/libs/hwui/RecordingCanvas.h @@ -52,8 +52,8 @@ public: // ---------------------------------------------------------------------------- // CanvasStateClient interface // ---------------------------------------------------------------------------- - virtual void onViewportInitialized() override {} - virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override {} + virtual void onViewportInitialized() override; + virtual void onSnapshotRestored(const Snapshot& removed, const Snapshot& restored) override; virtual GLuint getTargetFbo() const override { return -1; } // ---------------------------------------------------------------------------- diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 894a2bdf19df..351fbaa86a2a 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -929,7 +929,7 @@ void RenderNode::issueOperationsOfProjectedChildren(OpenGLRenderer& renderer, T& 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 + // If the projection receiver has an outline, we mask projected content to it // (which we know, apriori, are all tessellated paths) renderer.setProjectionPathMask(alloc, projectionReceiverOutline); diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index f824cc020196..abef806a6975 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -203,8 +203,8 @@ public: return RP_SET(mPrimitiveFields.mProjectBackwards, shouldProject); } - bool setProjectionReceiver(bool shouldRecieve) { - return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldRecieve); + bool setProjectionReceiver(bool shouldReceive) { + return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldReceive); } bool isProjectionReceiver() const { diff --git a/libs/hwui/Snapshot.h b/libs/hwui/Snapshot.h index aeeda965c48f..4789b338d173 100644 --- a/libs/hwui/Snapshot.h +++ b/libs/hwui/Snapshot.h @@ -158,13 +158,12 @@ public: /** * Returns the current clip in render target coordinates. */ - const Rect& getRenderTargetClip() { return mClipArea->getClipRect(); } + const Rect& getRenderTargetClip() const { return mClipArea->getClipRect(); } /* * Accessor functions so that the clip area can stay private */ bool clipIsEmpty() const { return mClipArea->isEmpty(); } - const Rect& getClipRect() const { return mClipArea->getClipRect(); } const SkRegion& getClipRegion() const { return mClipArea->getClipRegion(); } bool clipIsSimple() const { return mClipArea->isSimple(); } const ClipArea& getClipArea() const { return *mClipArea; } diff --git a/libs/hwui/microbench/OpReordererBench.cpp b/libs/hwui/microbench/OpReordererBench.cpp index 4c8dedfa5a15..cf96d44c286f 100644 --- a/libs/hwui/microbench/OpReordererBench.cpp +++ b/libs/hwui/microbench/OpReordererBench.cpp @@ -65,7 +65,7 @@ void BM_OpReorderer_deferAndRender::Run(int iters) { MicroBench::DoNotOptimize(&reorderer); BakedOpRenderer::Info info(caches, renderState, 200, 200, true); - reorderer.replayBakedOps<BakedOpRenderer>(&info); + reorderer.replayBakedOps<BakedOpRenderer>(info); } StopBenchmarkTiming(); }); diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 238cf06e77f6..f5714265032a 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -323,7 +323,7 @@ void CanvasContext::draw() { BakedOpRenderer::Info info(Caches::getInstance(), mRenderThread.renderState(), frame.width(), frame.height(), mOpaque); // TODO: profiler().draw(mCanvas); - reorderer.replayBakedOps<BakedOpRenderer>(&info); + reorderer.replayBakedOps<BakedOpRenderer>(info); bool drew = info.didDraw; diff --git a/libs/hwui/unit_tests/OpReordererTests.cpp b/libs/hwui/unit_tests/OpReordererTests.cpp index e1249fbd2e96..d02f89dd7088 100644 --- a/libs/hwui/unit_tests/OpReordererTests.cpp +++ b/libs/hwui/unit_tests/OpReordererTests.cpp @@ -27,26 +27,69 @@ namespace android { namespace uirenderer { -#define UNSUPPORTED_OP(Info, Type) \ - static void on##Type(Info*, const Type&, const BakedOpState&) { FAIL(); } +/** + * Class that redirects static operation dispatch to virtual methods on a Client class. + * + * The client is recreated for every op (so data cannot be persisted between operations), but the + * virtual dispatch allows for default behaviors to be specified without enumerating each operation + * for every test. + * + * onXXXOp methods fail by default - tests should override ops they expect + * startFrame/endFrame do nothing by default - tests should override to intercept + */ +template<class CustomClient, class Arg> +class TestReceiver { +public: +#define CLIENT_METHOD(Type) \ + virtual void on##Type(Arg&, const Type&, const BakedOpState&) { FAIL(); } + class Client { + public: + virtual ~Client() {}; + MAP_OPS(CLIENT_METHOD) + + virtual void startFrame(Arg& info) {} + virtual void endFrame(Arg& info) {} + }; + +#define DISPATCHER_METHOD(Type) \ + static void on##Type(Arg& arg, const Type& op, const BakedOpState& state) { \ + CustomClient client; client.on##Type(arg, op, state); \ + } + MAP_OPS(DISPATCHER_METHOD) + + static void startFrame(Arg& info) { + CustomClient client; + client.startFrame(info); + } + + static void endFrame(Arg& info) { + CustomClient client; + client.endFrame(info); + } +}; class Info { public: int index = 0; }; -class SimpleReceiver { +// Receiver class which will fail if it receives any ops +class FailReceiver : public TestReceiver<FailReceiver, Info>::Client {}; + +class SimpleReceiver : public TestReceiver<SimpleReceiver, Info>::Client { public: - static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) { - EXPECT_EQ(1, info->index++); + void startFrame(Info& info) override { + EXPECT_EQ(0, info.index++); } - static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) { - EXPECT_EQ(0, info->index++); + void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override { + EXPECT_EQ(1, info.index++); + } + void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override { + EXPECT_EQ(2, info.index++); + } + void endFrame(Info& info) override { + EXPECT_EQ(3, info.index++); } - UNSUPPORTED_OP(Info, RenderNodeOp) - UNSUPPORTED_OP(Info, SimpleRectsOp) - static void startFrame(Info& info) {} - static void endFrame(Info& info) {} }; TEST(OpReorderer, simple) { auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) { @@ -54,28 +97,39 @@ TEST(OpReorderer, simple) { canvas.drawRect(0, 0, 100, 200, SkPaint()); canvas.drawBitmap(bitmap, 10, 10, nullptr); }); + OpReorderer reorderer; + reorderer.defer(200, 200, *dl); + Info info; + reorderer.replayBakedOps<TestReceiver<SimpleReceiver, Info>>(info); + EXPECT_EQ(4, info.index); // 2 ops + start + end +} + + +TEST(OpReorderer, simpleRejection) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); // intersection should be empty + canvas.drawRect(0, 0, 400, 400, SkPaint()); + canvas.restore(); + }); OpReorderer reorderer; reorderer.defer(200, 200, *dl); Info info; - reorderer.replayBakedOps<SimpleReceiver>(&info); + reorderer.replayBakedOps<TestReceiver<FailReceiver, Info>>(info); } static int SIMPLE_BATCHING_LOOPS = 5; -class SimpleBatchingReceiver { +class SimpleBatchingReceiver : public TestReceiver<SimpleBatchingReceiver, Info>::Client { public: - static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) { - EXPECT_TRUE(info->index++ >= SIMPLE_BATCHING_LOOPS); + void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override { + 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); + void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override { + 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 dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { @@ -96,15 +150,14 @@ TEST(OpReorderer, simpleBatching) { reorderer.defer(200, 200, *dl); Info info; - reorderer.replayBakedOps<SimpleBatchingReceiver>(&info); + reorderer.replayBakedOps<TestReceiver<SimpleBatchingReceiver, Info>>(info); EXPECT_EQ(2 * SIMPLE_BATCHING_LOOPS, info.index); // 2 x loops ops, because no merging (TODO: force no merging) } -class RenderNodeReceiver { +class RenderNodeReceiver : public TestReceiver<RenderNodeReceiver, Info>::Client { public: - UNSUPPORTED_OP(Info, BitmapOp) - static void onRectOp(Info* info, const RectOp& op, const BakedOpState& state) { - switch(info->index++) { + void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override { + switch(info.index++) { case 0: EXPECT_EQ(Rect(0, 0, 200, 200), state.computedState.clippedBounds); EXPECT_EQ(SK_ColorDKGRAY, op.paint->getColor()); @@ -117,10 +170,6 @@ public: 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) { @@ -151,22 +200,17 @@ TEST(OpReorderer, renderNode) { reorderer.defer(SkRect::MakeWH(200, 200), 200, 200, nodes); Info info; - reorderer.replayBakedOps<RenderNodeReceiver>(&info); + reorderer.replayBakedOps<TestReceiver<RenderNodeReceiver, Info>>(info); } -class ClippedReceiver { +class ClippedReceiver : public TestReceiver<ClippedReceiver, Info>::Client { public: - static void onBitmapOp(Info* info, const BitmapOp& op, const BakedOpState& state) { - EXPECT_EQ(0, info->index++); + void onBitmapOp(Info& info, const BitmapOp& op, const BakedOpState& state) override { + EXPECT_EQ(0, info.index++); EXPECT_EQ(Rect(10, 20, 30, 40), state.computedState.clippedBounds); EXPECT_EQ(Rect(10, 20, 30, 40), state.computedState.clipRect); EXPECT_TRUE(state.computedState.transform.isIdentity()); } - UNSUPPORTED_OP(Info, RectOp) - UNSUPPORTED_OP(Info, RenderNodeOp) - UNSUPPORTED_OP(Info, SimpleRectsOp) - static void startFrame(Info& info) {} - static void endFrame(Info& info) {} }; TEST(OpReorderer, clipped) { sp<RenderNode> node = TestUtils::createNode<RecordingCanvas>(0, 0, 200, 200, [](RecordingCanvas& canvas) { @@ -182,8 +226,106 @@ TEST(OpReorderer, clipped) { 200, 200, nodes); Info info; - reorderer.replayBakedOps<ClippedReceiver>(&info); + reorderer.replayBakedOps<TestReceiver<ClippedReceiver, Info>>(info); } + +class SaveLayerSimpleReceiver : public TestReceiver<SaveLayerSimpleReceiver, Info>::Client { +public: + void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override { + EXPECT_EQ(0, info.index++); + EXPECT_EQ(Rect(10, 10, 190, 190), op.unmappedBounds); + EXPECT_EQ(Rect(0, 0, 180, 180), state.computedState.clippedBounds); + EXPECT_EQ(Rect(0, 0, 180, 180), state.computedState.clipRect); + + Matrix4 expectedTransform; + expectedTransform.loadTranslate(-10, -10, 0); + EXPECT_MATRIX_APPROX_EQ(expectedTransform, state.computedState.transform); + } + void onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) override { + EXPECT_EQ(1, info.index++); + EXPECT_EQ(Rect(10, 10, 190, 190), state.computedState.clippedBounds); + EXPECT_EQ(Rect(0, 0, 200, 200), state.computedState.clipRect); + EXPECT_TRUE(state.computedState.transform.isIdentity()); + } +}; +TEST(OpReorderer, saveLayerSimple) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.saveLayerAlpha(10, 10, 190, 190, 128, SkCanvas::kClipToLayer_SaveFlag); + canvas.drawRect(10, 10, 190, 190, SkPaint()); + canvas.restore(); + }); + + OpReorderer reorderer; + reorderer.defer(200, 200, *dl); + + Info info; + reorderer.replayBakedOps<TestReceiver<SaveLayerSimpleReceiver, Info>>(info); + EXPECT_EQ(2, info.index); } + + +// saveLayer1 {rect1, saveLayer2 { rect2 } } will play back as rect2, rect1, layerOp2, layerOp1 +class SaveLayerNestedReceiver : public TestReceiver<SaveLayerNestedReceiver, Info>::Client { +public: + void onRectOp(Info& info, const RectOp& op, const BakedOpState& state) override { + const int index = info.index++; + if (index == 0) { + EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); // inner rect + } else if (index == 1) { + EXPECT_EQ(Rect(0, 0, 800, 800), op.unmappedBounds); // outer rect + } else { FAIL(); } + } + void onLayerOp(Info& info, const LayerOp& op, const BakedOpState& state) override { + const int index = info.index++; + if (index == 2) { + EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); // inner layer + } else if (index == 3) { + EXPECT_EQ(Rect(0, 0, 800, 800), op.unmappedBounds); // outer layer + } else { FAIL(); } + } +}; +TEST(OpReorderer, saveLayerNested) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(800, 800, [](RecordingCanvas& canvas) { + canvas.saveLayerAlpha(0, 0, 800, 800, 128, SkCanvas::kClipToLayer_SaveFlag); + { + canvas.drawRect(0, 0, 800, 800, SkPaint()); + canvas.saveLayerAlpha(0, 0, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag); + { + canvas.drawRect(0, 0, 400, 400, SkPaint()); + } + canvas.restore(); + } + canvas.restore(); + }); + + OpReorderer reorderer; + reorderer.defer(800, 800, *dl); + + Info info; + reorderer.replayBakedOps<TestReceiver<SaveLayerNestedReceiver, Info>>(info); + EXPECT_EQ(4, info.index); +} + +TEST(OpReorderer, saveLayerContentRejection) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + canvas.clipRect(200, 200, 400, 400, SkRegion::kIntersect_Op); + canvas.saveLayerAlpha(200, 200, 400, 400, 128, SkCanvas::kClipToLayer_SaveFlag); + + // draw within save layer may still be recorded, but shouldn't be drawn + canvas.drawRect(200, 200, 400, 400, SkPaint()); + + canvas.restore(); + canvas.restore(); + }); + OpReorderer reorderer; + reorderer.defer(200, 200, *dl); + Info info; + + // should see no ops, even within the layer, since the layer should be rejected + reorderer.replayBakedOps<TestReceiver<FailReceiver, Info>>(info); } + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/unit_tests/RecordingCanvasTests.cpp b/libs/hwui/unit_tests/RecordingCanvasTests.cpp index ce25fc6189b4..c0231235027a 100644 --- a/libs/hwui/unit_tests/RecordingCanvasTests.cpp +++ b/libs/hwui/unit_tests/RecordingCanvasTests.cpp @@ -24,11 +24,11 @@ namespace android { namespace uirenderer { static void playbackOps(const DisplayList& displayList, - std::function<void(const RecordedOp&)> opReciever) { + std::function<void(const RecordedOp&)> opReceiver) { for (const DisplayList::Chunk& chunk : displayList.getChunks()) { for (size_t opIndex = chunk.beginOpIndex; opIndex < chunk.endOpIndex; opIndex++) { RecordedOp* op = displayList.getOps()[opIndex]; - opReciever(*op); + opReceiver(*op); } } } @@ -109,5 +109,123 @@ TEST(RecordingCanvas, backgroundAndImage) { ASSERT_EQ(2, count); // two draws observed } +TEST(RecordingCanvas, saveLayerSimple) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.saveLayerAlpha(10, 20, 190, 180, 128, SkCanvas::kARGB_ClipLayer_SaveFlag); + canvas.drawRect(10, 20, 190, 180, SkPaint()); + canvas.restore(); + }); + int count = 0; + playbackOps(*dl, [&count](const RecordedOp& op) { + Matrix4 expectedMatrix; + switch(count++) { + case 0: + EXPECT_EQ(RecordedOpId::BeginLayerOp, op.opId); + // TODO: add asserts + break; + case 1: + EXPECT_EQ(RecordedOpId::RectOp, op.opId); + EXPECT_EQ(Rect(0, 0, 180, 160), op.localClipRect); + EXPECT_EQ(Rect(10, 20, 190, 180), op.unmappedBounds); + expectedMatrix.loadTranslate(-10, -20, 0); + EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix); + break; + case 2: + EXPECT_EQ(RecordedOpId::EndLayerOp, op.opId); + // TODO: add asserts + break; + default: + FAIL(); + } + }); + EXPECT_EQ(3, count); } + +TEST(RecordingCanvas, saveLayerViewportCrop) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + // shouldn't matter, since saveLayer will clip to its bounds + canvas.clipRect(-1000, -1000, 1000, 1000, SkRegion::kReplace_Op); + + canvas.saveLayerAlpha(100, 100, 300, 300, 128, SkCanvas::kARGB_ClipLayer_SaveFlag); + canvas.drawRect(0, 0, 400, 400, SkPaint()); + canvas.restore(); + }); + int count = 0; + playbackOps(*dl, [&count](const RecordedOp& op) { + if (count++ == 1) { + Matrix4 expectedMatrix; + EXPECT_EQ(RecordedOpId::RectOp, op.opId); + + // recorded clip rect should be intersection of + // viewport and saveLayer bounds, in layer space + EXPECT_EQ(Rect(0, 0, 100, 100), op.localClipRect); + EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); + expectedMatrix.loadTranslate(-100, -100, 0); + EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix); + } + }); + EXPECT_EQ(3, count); } + +TEST(RecordingCanvas, saveLayerRotateUnclipped) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + canvas.translate(100, 100); + canvas.rotate(45); + canvas.translate(-50, -50); + + canvas.saveLayerAlpha(0, 0, 100, 100, 128, SkCanvas::kARGB_ClipLayer_SaveFlag); + canvas.drawRect(0, 0, 100, 100, SkPaint()); + canvas.restore(); + + canvas.restore(); + }); + int count = 0; + playbackOps(*dl, [&count](const RecordedOp& op) { + if (count++ == 1) { + Matrix4 expectedMatrix; + EXPECT_EQ(RecordedOpId::RectOp, op.opId); + + // recorded rect doesn't see rotate, since recorded relative to saveLayer bounds + EXPECT_EQ(Rect(0, 0, 100, 100), op.localClipRect); + EXPECT_EQ(Rect(0, 0, 100, 100), op.unmappedBounds); + expectedMatrix.loadIdentity(); + EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix); + } + }); + EXPECT_EQ(3, count); +} + +TEST(RecordingCanvas, saveLayerRotateClipped) { + auto dl = TestUtils::createDisplayList<RecordingCanvas>(200, 200, [](RecordingCanvas& canvas) { + canvas.save(SkCanvas::kMatrix_SaveFlag | SkCanvas::kClip_SaveFlag); + canvas.translate(100, 100); + canvas.rotate(45); + canvas.translate(-200, -200); + + // area of saveLayer will be clipped to parent viewport, so we ask for 400x400... + canvas.saveLayerAlpha(0, 0, 400, 400, 128, SkCanvas::kARGB_ClipLayer_SaveFlag); + canvas.drawRect(0, 0, 400, 400, SkPaint()); + canvas.restore(); + + canvas.restore(); + }); + int count = 0; + playbackOps(*dl, [&count](const RecordedOp& op) { + if (count++ == 1) { + Matrix4 expectedMatrix; + EXPECT_EQ(RecordedOpId::RectOp, op.opId); + + // ...and get about 58.6, 58.6, 341.4 341.4, because the bounds are clipped by + // the parent 200x200 viewport, but prior to rotation + EXPECT_RECT_APPROX_EQ(Rect(58.57864, 58.57864, 341.42136, 341.42136), op.localClipRect); + EXPECT_EQ(Rect(0, 0, 400, 400), op.unmappedBounds); + expectedMatrix.loadIdentity(); + EXPECT_MATRIX_APPROX_EQ(expectedMatrix, op.localMatrix); + } + }); + EXPECT_EQ(3, count); +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/unit_tests/TestUtils.h b/libs/hwui/unit_tests/TestUtils.h index 80d83a2e47a1..99ecc9b99284 100644 --- a/libs/hwui/unit_tests/TestUtils.h +++ b/libs/hwui/unit_tests/TestUtils.h @@ -31,6 +31,12 @@ namespace uirenderer { #define EXPECT_MATRIX_APPROX_EQ(a, b) \ EXPECT_TRUE(TestUtils::matricesAreApproxEqual(a, b)) +#define EXPECT_RECT_APPROX_EQ(a, b) \ + EXPECT_TRUE(MathUtils::areEqual(a.left, b.left) \ + && MathUtils::areEqual(a.top, b.top) \ + && MathUtils::areEqual(a.right, b.right) \ + && MathUtils::areEqual(a.bottom, b.bottom)); + class TestUtils { public: static bool matricesAreApproxEqual(const Matrix4& a, const Matrix4& b) { |