diff options
author | 2015-10-20 09:39:42 -0700 | |
---|---|---|
committer | 2015-10-21 18:19:37 -0700 | |
commit | 6fe991e5e76f9af9dab960100d5768d96d5f4daa (patch) | |
tree | 20616f2c371e188492b48a9c759530b06047550a | |
parent | 387d1b25b58bb2bb80d983b7db7ddac22731a552 (diff) |
Work to support saveLayer in new pipeline
clipped SaveLayers will now be pulled to the beginning of the frame,
prior to drawing FBO 0. This will remove the need for switching FBOs
mid-frame.
Change-Id: I4d8dc1f845e84e9b49d5acdf4f4703eef4a9cb06
-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) { |