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
This commit is contained in:
@@ -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<BakedOpState>(
|
||||
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<BakedOpState>(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<BakedOpState>(
|
||||
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<BakedOpState>(allocator, snapshot, shadowOpPtr);
|
||||
}
|
||||
|
||||
BakedOpState* BakedOpState::directConstruct(LinearAllocator& allocator,
|
||||
const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp) {
|
||||
return allocator.create_trivial<BakedOpState>(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
|
||||
|
||||
@@ -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<BakedOpState>(
|
||||
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<BakedOpState>(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<BakedOpState>(
|
||||
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<BakedOpState>(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<BakedOpState>(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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -201,7 +201,7 @@ private:
|
||||
return mAllocator.create<SkPath>();
|
||||
}
|
||||
|
||||
const BakedOpState* deferStrokeableOp(const RecordedOp& op, batchid_t batchId,
|
||||
BakedOpState* deferStrokeableOp(const RecordedOp& op, batchid_t batchId,
|
||||
BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<BakedOpState*> activeUnclippedSaveLayers;
|
||||
private:
|
||||
void onDeferOp(LinearAllocator& allocator, const BakedOpState* bakedState);
|
||||
void flushLayerClears(LinearAllocator& allocator);
|
||||
|
||||
std::vector<BatchBase*> mBatches;
|
||||
|
||||
@@ -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<TestDispatcher>(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<TestDispatcher>(renderer);
|
||||
EXPECT_EQ(2, renderer.getIndex()) << "Expect exactly one op";
|
||||
}
|
||||
|
||||
RENDERTHREAD_TEST(FrameBuilder, clippedMerging) {
|
||||
class ClippedMergingTestRenderer : public TestRendererBase {
|
||||
public:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user