From 80d2ade939153da87b3cd3b0a69a713bf68b64ba Mon Sep 17 00:00:00 2001 From: Chris Craik Date: Mon, 28 Mar 2016 12:54:07 -0700 Subject: [PATCH] Overdraw avoidance in new pipeline bug:27873093 Adds the simple overdraw avoidance optimization to the new pipeline. This means when LayerBuilder defers draws that are opaque over the full area of the repaint region, it will discard all drawing content beneth. Also moves a lot of complexity out of BakedOpState's header. Change-Id: Iffca6d8e1b170ef31a5d6c83d25592670e02323d --- libs/hwui/BakedOpState.cpp | 58 +++++++++++++++++ libs/hwui/BakedOpState.h | 49 +++----------- libs/hwui/FrameBuilder.cpp | 8 ++- libs/hwui/FrameBuilder.h | 2 +- libs/hwui/LayerBuilder.cpp | 34 +++++++--- libs/hwui/LayerBuilder.h | 5 +- libs/hwui/tests/unit/FrameBuilderTests.cpp | 74 ++++++++++++++++++++++ libs/hwui/utils/PaintUtils.h | 15 +++++ 8 files changed, 191 insertions(+), 54 deletions(-) diff --git a/libs/hwui/BakedOpState.cpp b/libs/hwui/BakedOpState.cpp index 859036543b4a3..b70d5868c7185 100644 --- a/libs/hwui/BakedOpState.cpp +++ b/libs/hwui/BakedOpState.cpp @@ -108,5 +108,63 @@ ResolvedRenderState::ResolvedRenderState(const ClipRect* clipRect, const Rect& d clippedBounds.doIntersect(clipRect->rect); } +BakedOpState* BakedOpState::tryConstruct(LinearAllocator& allocator, + Snapshot& snapshot, const RecordedOp& recordedOp) { + if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; + BakedOpState* bakedState = allocator.create_trivial( + allocator, snapshot, recordedOp, false); + if (bakedState->computedState.clippedBounds.isEmpty()) { + // bounds are empty, so op is rejected + allocator.rewindIfLastAlloc(bakedState); + return nullptr; + } + return bakedState; +} + +BakedOpState* BakedOpState::tryConstructUnbounded(LinearAllocator& allocator, + Snapshot& snapshot, const RecordedOp& recordedOp) { + if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; + return allocator.create_trivial(allocator, snapshot, recordedOp); +} + +BakedOpState* BakedOpState::tryStrokeableOpConstruct(LinearAllocator& allocator, + Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior) { + if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; + bool expandForStroke = (strokeBehavior == StrokeBehavior::StyleDefined) + ? (recordedOp.paint && recordedOp.paint->getStyle() != SkPaint::kFill_Style) + : true; + + BakedOpState* bakedState = allocator.create_trivial( + allocator, snapshot, recordedOp, expandForStroke); + if (bakedState->computedState.clippedBounds.isEmpty()) { + // bounds are empty, so op is rejected + // NOTE: this won't succeed if a clip was allocated + allocator.rewindIfLastAlloc(bakedState); + return nullptr; + } + return bakedState; +} + +BakedOpState* BakedOpState::tryShadowOpConstruct(LinearAllocator& allocator, + Snapshot& snapshot, const ShadowOp* shadowOpPtr) { + if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; + + // clip isn't empty, so construct the op + return allocator.create_trivial(allocator, snapshot, shadowOpPtr); +} + +BakedOpState* BakedOpState::directConstruct(LinearAllocator& allocator, + const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp) { + return allocator.create_trivial(clip, dstRect, recordedOp); +} + +void BakedOpState::setupOpacity(const SkPaint* paint) { + computedState.opaqueOverClippedBounds = computedState.transform.isSimple() + && computedState.clipState->mode == ClipMode::Rectangle + && MathUtils::areEqual(alpha, 1.0f) + && !roundRectClipState + && PaintUtils::isOpaquePaint(paint); +} + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h index 4e3cb8a15e241..e1441fca5ee27 100644 --- a/libs/hwui/BakedOpState.h +++ b/libs/hwui/BakedOpState.h @@ -93,6 +93,7 @@ public: Rect clippedBounds; int clipSideFlags = 0; const SkPath* localProjectionPathMask = nullptr; + bool opaqueOverClippedBounds = false; }; /** @@ -103,23 +104,10 @@ public: class BakedOpState { public: static BakedOpState* tryConstruct(LinearAllocator& allocator, - Snapshot& snapshot, const RecordedOp& recordedOp) { - if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; - BakedOpState* bakedState = allocator.create_trivial( - allocator, snapshot, recordedOp, false); - if (bakedState->computedState.clippedBounds.isEmpty()) { - // bounds are empty, so op is rejected - allocator.rewindIfLastAlloc(bakedState); - return nullptr; - } - return bakedState; - } + Snapshot& snapshot, const RecordedOp& recordedOp); static BakedOpState* tryConstructUnbounded(LinearAllocator& allocator, - Snapshot& snapshot, const RecordedOp& recordedOp) { - if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; - return allocator.create_trivial(allocator, snapshot, recordedOp); - } + Snapshot& snapshot, const RecordedOp& recordedOp); enum class StrokeBehavior { // stroking is forced, regardless of style on paint (such as for lines) @@ -129,35 +117,16 @@ public: }; static BakedOpState* tryStrokeableOpConstruct(LinearAllocator& allocator, - Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior) { - if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; - bool expandForStroke = (strokeBehavior == StrokeBehavior::StyleDefined) - ? (recordedOp.paint && recordedOp.paint->getStyle() != SkPaint::kFill_Style) - : true; - - BakedOpState* bakedState = allocator.create_trivial( - allocator, snapshot, recordedOp, expandForStroke); - if (bakedState->computedState.clippedBounds.isEmpty()) { - // bounds are empty, so op is rejected - // NOTE: this won't succeed if a clip was allocated - allocator.rewindIfLastAlloc(bakedState); - return nullptr; - } - return bakedState; - } + Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior); static BakedOpState* tryShadowOpConstruct(LinearAllocator& allocator, - Snapshot& snapshot, const ShadowOp* shadowOpPtr) { - if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr; - - // clip isn't empty, so construct the op - return allocator.create_trivial(allocator, snapshot, shadowOpPtr); - } + Snapshot& snapshot, const ShadowOp* shadowOpPtr); static BakedOpState* directConstruct(LinearAllocator& allocator, - const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp) { - return allocator.create_trivial(clip, dstRect, recordedOp); - } + const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp); + + // Set opaqueOverClippedBounds. If this method isn't called, the op is assumed translucent. + void setupOpacity(const SkPaint* paint); // computed state: ResolvedRenderState computedState; diff --git a/libs/hwui/FrameBuilder.cpp b/libs/hwui/FrameBuilder.cpp index b1314feebf34c..b18836f175eab 100644 --- a/libs/hwui/FrameBuilder.cpp +++ b/libs/hwui/FrameBuilder.cpp @@ -481,12 +481,17 @@ void FrameBuilder::deferRenderNodeOp(const RenderNodeOp& op) { * Defers an unmergeable, strokeable op, accounting correctly * for paint's style on the bounds being computed. */ -const BakedOpState* FrameBuilder::deferStrokeableOp(const RecordedOp& op, batchid_t batchId, +BakedOpState* FrameBuilder::deferStrokeableOp(const RecordedOp& op, batchid_t batchId, BakedOpState::StrokeBehavior strokeBehavior) { // Note: here we account for stroke when baking the op BakedOpState* bakedState = BakedOpState::tryStrokeableOpConstruct( mAllocator, *mCanvasState.writableSnapshot(), op, strokeBehavior); if (!bakedState) return nullptr; // quick rejected + + if (op.opId == RecordedOpId::RectOp && op.paint->getStyle() != SkPaint::kStroke_Style) { + bakedState->setupOpacity(op.paint); + } + currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId); return bakedState; } @@ -516,6 +521,7 @@ static bool hasMergeableClip(const BakedOpState& state) { void FrameBuilder::deferBitmapOp(const BitmapOp& op) { BakedOpState* bakedState = tryBakeOpState(op); if (!bakedState) return; // quick rejected + bakedState->setupOpacity(op.paint); // Don't merge non-simply transformed or neg scale ops, SET_TEXTURE doesn't handle rotation // Don't merge A8 bitmaps - the paint's color isn't compared by mergeId, or in diff --git a/libs/hwui/FrameBuilder.h b/libs/hwui/FrameBuilder.h index 0b7a6062456a7..02c05cb1bbbe4 100644 --- a/libs/hwui/FrameBuilder.h +++ b/libs/hwui/FrameBuilder.h @@ -201,7 +201,7 @@ private: return mAllocator.create(); } - const BakedOpState* deferStrokeableOp(const RecordedOp& op, batchid_t batchId, + BakedOpState* deferStrokeableOp(const RecordedOp& op, batchid_t batchId, BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined); /** diff --git a/libs/hwui/LayerBuilder.cpp b/libs/hwui/LayerBuilder.cpp index e6a95ff177a40..eea11bff7d8a4 100644 --- a/libs/hwui/LayerBuilder.cpp +++ b/libs/hwui/LayerBuilder.cpp @@ -236,6 +236,21 @@ void LayerBuilder::deferLayerClear(const Rect& rect) { mClearRects.push_back(rect); } +void LayerBuilder::onDeferOp(LinearAllocator& allocator, const BakedOpState* bakedState) { + if (bakedState->op->opId != RecordedOpId::CopyToLayerOp) { + // First non-CopyToLayer, so stop stashing up layer clears for unclipped save layers, + // and issue them together in one draw. + flushLayerClears(allocator); + + if (CC_UNLIKELY(activeUnclippedSaveLayers.empty() + && bakedState->computedState.opaqueOverClippedBounds + && bakedState->computedState.clippedBounds.contains(repaintRect))) { + // discard all deferred drawing ops, since new one will occlude them + clear(); + } + } +} + void LayerBuilder::flushLayerClears(LinearAllocator& allocator) { if (CC_UNLIKELY(!mClearRects.empty())) { const int vertCount = mClearRects.size() * 4; @@ -270,11 +285,7 @@ void LayerBuilder::flushLayerClears(LinearAllocator& allocator) { void LayerBuilder::deferUnmergeableOp(LinearAllocator& allocator, BakedOpState* op, batchid_t batchId) { - if (batchId != OpBatchType::CopyToLayer) { - // if first op after one or more unclipped saveLayers, flush the layer clears - flushLayerClears(allocator); - } - + onDeferOp(allocator, op); OpBatch* targetBatch = mBatchLookup[batchId]; size_t insertBatchIndex = mBatches.size(); @@ -295,10 +306,7 @@ void LayerBuilder::deferUnmergeableOp(LinearAllocator& allocator, void LayerBuilder::deferMergeableOp(LinearAllocator& allocator, BakedOpState* op, batchid_t batchId, mergeid_t mergeId) { - if (batchId != OpBatchType::CopyToLayer) { - // if first op after one or more unclipped saveLayers, flush the layer clears - flushLayerClears(allocator); - } + onDeferOp(allocator, op); MergingOpBatch* targetBatch = nullptr; // Try to merge with any existing batch with same mergeId @@ -348,6 +356,14 @@ void LayerBuilder::replayBakedOpsImpl(void* arg, } } +void LayerBuilder::clear() { + mBatches.clear(); + for (int i = 0; i < OpBatchType::Count; i++) { + mBatchLookup[i] = nullptr; + mMergingBatchLookup[i].clear(); + } +} + void LayerBuilder::dump() const { ALOGD("LayerBuilder %p, %ux%u buffer %p, blo %p, rn %p (%s)", this, width, height, offscreenBuffer, beginLayerOp, diff --git a/libs/hwui/LayerBuilder.h b/libs/hwui/LayerBuilder.h index 4a7ca2de9b1b5..4de432c5e7be5 100644 --- a/libs/hwui/LayerBuilder.h +++ b/libs/hwui/LayerBuilder.h @@ -100,9 +100,7 @@ public: return mBatches.empty(); } - void clear() { - mBatches.clear(); - } + void clear(); void dump() const; @@ -117,6 +115,7 @@ public: // list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps std::vector activeUnclippedSaveLayers; private: + void onDeferOp(LinearAllocator& allocator, const BakedOpState* bakedState); void flushLayerClears(LinearAllocator& allocator); std::vector mBatches; diff --git a/libs/hwui/tests/unit/FrameBuilderTests.cpp b/libs/hwui/tests/unit/FrameBuilderTests.cpp index e97aaa6ac6881..ba22f91cde9d2 100644 --- a/libs/hwui/tests/unit/FrameBuilderTests.cpp +++ b/libs/hwui/tests/unit/FrameBuilderTests.cpp @@ -216,6 +216,80 @@ RENDERTHREAD_TEST(FrameBuilder, simpleBatching) { << "Expect number of ops = 2 * loop count"; } +RENDERTHREAD_TEST(FrameBuilder, avoidOverdraw_rects) { + class AvoidOverdrawRectsTestRenderer : public TestRendererBase { + public: + void onRectOp(const RectOp& op, const BakedOpState& state) override { + EXPECT_EQ(mIndex++, 0) << "Should be one rect"; + EXPECT_EQ(Rect(10, 10, 190, 190), op.unmappedBounds) + << "Last rect should occlude others."; + } + }; + auto node = TestUtils::createNode(0, 0, 200, 200, + [](RenderProperties& props, RecordingCanvas& canvas) { + canvas.drawRect(0, 0, 200, 200, SkPaint()); + canvas.drawRect(0, 0, 200, 200, SkPaint()); + canvas.drawRect(10, 10, 190, 190, SkPaint()); + }); + + // Damage (and therefore clip) is same as last draw, subset of renderable area. + // This means last op occludes other contents, and they'll be rejected to avoid overdraw. + SkRect damageRect = SkRect::MakeLTRB(10, 10, 190, 190); + FrameBuilder frameBuilder(sEmptyLayerUpdateQueue, damageRect, 200, 200, + TestUtils::createSyncedNodeList(node), sLightGeometry, Caches::getInstance()); + + EXPECT_EQ(3u, node->getDisplayList()->getOps().size()) + << "Recording must not have rejected ops, in order for this test to be valid"; + + AvoidOverdrawRectsTestRenderer renderer; + frameBuilder.replayBakedOps(renderer); + EXPECT_EQ(1, renderer.getIndex()) << "Expect exactly one op"; +} + +RENDERTHREAD_TEST(FrameBuilder, avoidOverdraw_bitmaps) { + static SkBitmap opaqueBitmap = TestUtils::createSkBitmap(50, 50, + SkColorType::kRGB_565_SkColorType); + static SkBitmap transpBitmap = TestUtils::createSkBitmap(50, 50, + SkColorType::kAlpha_8_SkColorType); + class AvoidOverdrawBitmapsTestRenderer : public TestRendererBase { + public: + void onBitmapOp(const BitmapOp& op, const BakedOpState& state) override { + EXPECT_LT(mIndex++, 2) << "Should be two bitmaps"; + switch(mIndex++) { + case 0: + EXPECT_EQ(opaqueBitmap.pixelRef(), op.bitmap->pixelRef()); + break; + case 1: + EXPECT_EQ(transpBitmap.pixelRef(), op.bitmap->pixelRef()); + break; + default: + ADD_FAILURE() << "Only two ops expected."; + } + } + }; + + auto node = TestUtils::createNode(0, 0, 50, 50, + [](RenderProperties& props, RecordingCanvas& canvas) { + canvas.drawRect(0, 0, 50, 50, SkPaint()); + canvas.drawRect(0, 0, 50, 50, SkPaint()); + canvas.drawBitmap(transpBitmap, 0, 0, nullptr); + + // only the below draws should remain, since they're + canvas.drawBitmap(opaqueBitmap, 0, 0, nullptr); + canvas.drawBitmap(transpBitmap, 0, 0, nullptr); + }); + + FrameBuilder frameBuilder(sEmptyLayerUpdateQueue, SkRect::MakeWH(50, 50), 50, 50, + TestUtils::createSyncedNodeList(node), sLightGeometry, Caches::getInstance()); + + EXPECT_EQ(5u, node->getDisplayList()->getOps().size()) + << "Recording must not have rejected ops, in order for this test to be valid"; + + AvoidOverdrawBitmapsTestRenderer renderer; + frameBuilder.replayBakedOps(renderer); + EXPECT_EQ(2, renderer.getIndex()) << "Expect exactly one op"; +} + RENDERTHREAD_TEST(FrameBuilder, clippedMerging) { class ClippedMergingTestRenderer : public TestRendererBase { public: diff --git a/libs/hwui/utils/PaintUtils.h b/libs/hwui/utils/PaintUtils.h index db537130e12e7..4faab9a5f6480 100644 --- a/libs/hwui/utils/PaintUtils.h +++ b/libs/hwui/utils/PaintUtils.h @@ -67,6 +67,21 @@ public: && getXfermode(paint.getXfermode()) == SkXfermode::kSrcOver_Mode; } + static bool isOpaquePaint(const SkPaint* paint) { + if (!paint) return true; // default (paintless) behavior is SrcOver, black + + if (paint->getAlpha() != 0xFF + || PaintUtils::isBlendedShader(paint->getShader()) + || PaintUtils::isBlendedColorFilter(paint->getColorFilter())) { + return false; + } + + // Only let simple srcOver / src blending modes declare opaque, since behavior is clear. + SkXfermode::Mode mode = getXfermode(paint->getXfermode()); + return mode == SkXfermode::Mode::kSrcOver_Mode + || mode == SkXfermode::Mode::kSrc_Mode; + } + static bool isBlendedShader(const SkShader* shader) { if (shader == nullptr) { return false;