Merge "Overdraw avoidance in new pipeline" into nyc-dev
This commit is contained in:
@@ -108,5 +108,63 @@ ResolvedRenderState::ResolvedRenderState(const ClipRect* clipRect, const Rect& d
|
|||||||
clippedBounds.doIntersect(clipRect->rect);
|
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 uirenderer
|
||||||
} // namespace android
|
} // namespace android
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ public:
|
|||||||
Rect clippedBounds;
|
Rect clippedBounds;
|
||||||
int clipSideFlags = 0;
|
int clipSideFlags = 0;
|
||||||
const SkPath* localProjectionPathMask = nullptr;
|
const SkPath* localProjectionPathMask = nullptr;
|
||||||
|
bool opaqueOverClippedBounds = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,23 +104,10 @@ public:
|
|||||||
class BakedOpState {
|
class BakedOpState {
|
||||||
public:
|
public:
|
||||||
static BakedOpState* tryConstruct(LinearAllocator& allocator,
|
static BakedOpState* tryConstruct(LinearAllocator& allocator,
|
||||||
Snapshot& snapshot, const RecordedOp& recordedOp) {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BakedOpState* tryConstructUnbounded(LinearAllocator& allocator,
|
static BakedOpState* tryConstructUnbounded(LinearAllocator& allocator,
|
||||||
Snapshot& snapshot, const RecordedOp& recordedOp) {
|
Snapshot& snapshot, const RecordedOp& recordedOp);
|
||||||
if (CC_UNLIKELY(snapshot.getRenderTargetClip().isEmpty())) return nullptr;
|
|
||||||
return allocator.create_trivial<BakedOpState>(allocator, snapshot, recordedOp);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class StrokeBehavior {
|
enum class StrokeBehavior {
|
||||||
// stroking is forced, regardless of style on paint (such as for lines)
|
// stroking is forced, regardless of style on paint (such as for lines)
|
||||||
@@ -129,35 +117,16 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
static BakedOpState* tryStrokeableOpConstruct(LinearAllocator& allocator,
|
static BakedOpState* tryStrokeableOpConstruct(LinearAllocator& allocator,
|
||||||
Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior) {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BakedOpState* tryShadowOpConstruct(LinearAllocator& allocator,
|
static BakedOpState* tryShadowOpConstruct(LinearAllocator& allocator,
|
||||||
Snapshot& snapshot, const ShadowOp* shadowOpPtr) {
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static BakedOpState* directConstruct(LinearAllocator& allocator,
|
static BakedOpState* directConstruct(LinearAllocator& allocator,
|
||||||
const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp) {
|
const ClipRect* clip, const Rect& dstRect, const RecordedOp& recordedOp);
|
||||||
return allocator.create_trivial<BakedOpState>(clip, dstRect, recordedOp);
|
|
||||||
}
|
// Set opaqueOverClippedBounds. If this method isn't called, the op is assumed translucent.
|
||||||
|
void setupOpacity(const SkPaint* paint);
|
||||||
|
|
||||||
// computed state:
|
// computed state:
|
||||||
ResolvedRenderState computedState;
|
ResolvedRenderState computedState;
|
||||||
|
|||||||
@@ -481,12 +481,17 @@ void FrameBuilder::deferRenderNodeOp(const RenderNodeOp& op) {
|
|||||||
* Defers an unmergeable, strokeable op, accounting correctly
|
* Defers an unmergeable, strokeable op, accounting correctly
|
||||||
* for paint's style on the bounds being computed.
|
* 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) {
|
BakedOpState::StrokeBehavior strokeBehavior) {
|
||||||
// Note: here we account for stroke when baking the op
|
// Note: here we account for stroke when baking the op
|
||||||
BakedOpState* bakedState = BakedOpState::tryStrokeableOpConstruct(
|
BakedOpState* bakedState = BakedOpState::tryStrokeableOpConstruct(
|
||||||
mAllocator, *mCanvasState.writableSnapshot(), op, strokeBehavior);
|
mAllocator, *mCanvasState.writableSnapshot(), op, strokeBehavior);
|
||||||
if (!bakedState) return nullptr; // quick rejected
|
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);
|
currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId);
|
||||||
return bakedState;
|
return bakedState;
|
||||||
}
|
}
|
||||||
@@ -516,6 +521,7 @@ static bool hasMergeableClip(const BakedOpState& state) {
|
|||||||
void FrameBuilder::deferBitmapOp(const BitmapOp& op) {
|
void FrameBuilder::deferBitmapOp(const BitmapOp& op) {
|
||||||
BakedOpState* bakedState = tryBakeOpState(op);
|
BakedOpState* bakedState = tryBakeOpState(op);
|
||||||
if (!bakedState) return; // quick rejected
|
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 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
|
// 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>();
|
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);
|
BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -236,6 +236,21 @@ void LayerBuilder::deferLayerClear(const Rect& rect) {
|
|||||||
mClearRects.push_back(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) {
|
void LayerBuilder::flushLayerClears(LinearAllocator& allocator) {
|
||||||
if (CC_UNLIKELY(!mClearRects.empty())) {
|
if (CC_UNLIKELY(!mClearRects.empty())) {
|
||||||
const int vertCount = mClearRects.size() * 4;
|
const int vertCount = mClearRects.size() * 4;
|
||||||
@@ -270,11 +285,7 @@ void LayerBuilder::flushLayerClears(LinearAllocator& allocator) {
|
|||||||
|
|
||||||
void LayerBuilder::deferUnmergeableOp(LinearAllocator& allocator,
|
void LayerBuilder::deferUnmergeableOp(LinearAllocator& allocator,
|
||||||
BakedOpState* op, batchid_t batchId) {
|
BakedOpState* op, batchid_t batchId) {
|
||||||
if (batchId != OpBatchType::CopyToLayer) {
|
onDeferOp(allocator, op);
|
||||||
// if first op after one or more unclipped saveLayers, flush the layer clears
|
|
||||||
flushLayerClears(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
OpBatch* targetBatch = mBatchLookup[batchId];
|
OpBatch* targetBatch = mBatchLookup[batchId];
|
||||||
|
|
||||||
size_t insertBatchIndex = mBatches.size();
|
size_t insertBatchIndex = mBatches.size();
|
||||||
@@ -295,10 +306,7 @@ void LayerBuilder::deferUnmergeableOp(LinearAllocator& allocator,
|
|||||||
|
|
||||||
void LayerBuilder::deferMergeableOp(LinearAllocator& allocator,
|
void LayerBuilder::deferMergeableOp(LinearAllocator& allocator,
|
||||||
BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
|
BakedOpState* op, batchid_t batchId, mergeid_t mergeId) {
|
||||||
if (batchId != OpBatchType::CopyToLayer) {
|
onDeferOp(allocator, op);
|
||||||
// if first op after one or more unclipped saveLayers, flush the layer clears
|
|
||||||
flushLayerClears(allocator);
|
|
||||||
}
|
|
||||||
MergingOpBatch* targetBatch = nullptr;
|
MergingOpBatch* targetBatch = nullptr;
|
||||||
|
|
||||||
// Try to merge with any existing batch with same mergeId
|
// 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 {
|
void LayerBuilder::dump() const {
|
||||||
ALOGD("LayerBuilder %p, %ux%u buffer %p, blo %p, rn %p (%s)",
|
ALOGD("LayerBuilder %p, %ux%u buffer %p, blo %p, rn %p (%s)",
|
||||||
this, width, height, offscreenBuffer, beginLayerOp,
|
this, width, height, offscreenBuffer, beginLayerOp,
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ public:
|
|||||||
return mBatches.empty();
|
return mBatches.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear();
|
||||||
mBatches.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void dump() const;
|
void dump() const;
|
||||||
|
|
||||||
@@ -117,6 +115,7 @@ public:
|
|||||||
// list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps
|
// list of deferred CopyFromLayer ops, to be deferred upon encountering EndUnclippedLayerOps
|
||||||
std::vector<BakedOpState*> activeUnclippedSaveLayers;
|
std::vector<BakedOpState*> activeUnclippedSaveLayers;
|
||||||
private:
|
private:
|
||||||
|
void onDeferOp(LinearAllocator& allocator, const BakedOpState* bakedState);
|
||||||
void flushLayerClears(LinearAllocator& allocator);
|
void flushLayerClears(LinearAllocator& allocator);
|
||||||
|
|
||||||
std::vector<BatchBase*> mBatches;
|
std::vector<BatchBase*> mBatches;
|
||||||
|
|||||||
@@ -216,6 +216,80 @@ RENDERTHREAD_TEST(FrameBuilder, simpleBatching) {
|
|||||||
<< "Expect number of ops = 2 * loop count";
|
<< "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) {
|
RENDERTHREAD_TEST(FrameBuilder, clippedMerging) {
|
||||||
class ClippedMergingTestRenderer : public TestRendererBase {
|
class ClippedMergingTestRenderer : public TestRendererBase {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ public:
|
|||||||
&& getXfermode(paint.getXfermode()) == SkXfermode::kSrcOver_Mode;
|
&& 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) {
|
static bool isBlendedShader(const SkShader* shader) {
|
||||||
if (shader == nullptr) {
|
if (shader == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user