blob: 682e8b7393567664822e5d6d5c1658c03af0250e [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.h"
#include <initializer_list>
#include "cc/paint/display_item_list.h"
#include "cc/paint/paint_filter.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_op_buffer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/graphics/paint/clip_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/display_item_list.h"
#include "third_party/blink/renderer/platform/graphics/paint/drawing_display_item.h"
#include "third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_chunk.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_chunk_subset.h"
#include "third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h"
#include "third_party/blink/renderer/platform/testing/fake_display_item_client.h"
#include "third_party/blink/renderer/platform/testing/paint_property_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/paint_test_configurations.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
void PrintTo(const Vector<cc::PaintOpType>& ops, std::ostream* os) {
*os << "[";
bool first = true;
for (auto op : ops) {
if (first)
first = false;
else
*os << ", ";
*os << op;
}
*os << "]";
}
void PrintTo(const cc::PaintRecord& record, std::ostream* os) {
Vector<cc::PaintOpType> ops;
for (const auto* op : cc::PaintOpBuffer::Iterator(&record))
ops.push_back(op->GetType());
PrintTo(ops, os);
}
void PrintTo(const SkRect& rect, std::ostream* os) {
*os << (cc::PaintOp::IsUnsetRect(rect) ? "(unset)"
: blink::FloatRect(rect).ToString());
}
namespace blink {
namespace {
class PaintChunksToCcLayerTest : public testing::Test,
public PaintTestConfigurations {};
INSTANTIATE_PAINT_TEST_SUITE_P(PaintChunksToCcLayerTest);
// Matches PaintOpTypes in a PaintRecord.
class PaintRecordMatcher
: public testing::MatcherInterface<const cc::PaintOpBuffer&> {
public:
static testing::Matcher<const cc::PaintOpBuffer&> Make(
std::initializer_list<cc::PaintOpType> args) {
return testing::MakeMatcher(new PaintRecordMatcher(args));
}
PaintRecordMatcher(std::initializer_list<cc::PaintOpType> args)
: expected_ops_(args) {}
bool MatchAndExplain(const cc::PaintOpBuffer& buffer,
testing::MatchResultListener* listener) const override {
size_t index = 0;
for (cc::PaintOpBuffer::Iterator it(&buffer); it; ++it, ++index) {
auto op = (*it)->GetType();
if (index < expected_ops_.size() && expected_ops_[index] == op)
continue;
if (listener->IsInterested()) {
*listener << "unexpected op " << op << " at index " << index << ",";
if (index < expected_ops_.size())
*listener << " expecting " << expected_ops_[index] << ".";
else
*listener << " expecting end of list.";
}
return false;
}
if (index == expected_ops_.size())
return true;
if (listener->IsInterested()) {
*listener << "unexpected end of list, expecting " << expected_ops_[index]
<< " at index " << index << ".";
}
return false;
}
void DescribeTo(::std::ostream* os) const override {
PrintTo(expected_ops_, os);
}
private:
Vector<cc::PaintOpType> expected_ops_;
};
#define EXPECT_EFFECT_BOUNDS(x, y, width, height, op_buffer, index) \
do { \
FloatRect bounds; \
if (const auto* save_layer_alpha = \
(op_buffer).GetOpAtForTesting<cc::SaveLayerAlphaOp>(index)) { \
bounds = save_layer_alpha->bounds; \
} else if (const auto* save_layer = \
(op_buffer).GetOpAtForTesting<cc::SaveLayerOp>(index)) { \
bounds = save_layer->bounds; \
} else { \
FAIL() << "No SaveLayer[Alpha]Op at " << index; \
} \
EXPECT_EQ(FloatRect(x, y, width, height), bounds); \
} while (false)
#define EXPECT_TRANSFORM_MATRIX(transform, op_buffer, index) \
do { \
const auto* concat = (op_buffer).GetOpAtForTesting<cc::ConcatOp>(index); \
ASSERT_NE(nullptr, concat); \
EXPECT_EQ(TransformationMatrix::ToSkM44(transform), concat->matrix); \
} while (false)
#define EXPECT_TRANSLATE(x, y, op_buffer, index) \
do { \
const auto* translate = \
(op_buffer).GetOpAtForTesting<cc::TranslateOp>(index); \
ASSERT_NE(nullptr, translate); \
EXPECT_EQ(x, translate->dx); \
EXPECT_EQ(y, translate->dy); \
} while (false)
#define EXPECT_CLIP(r, op_buffer, index) \
do { \
const auto* clip_op = \
(op_buffer).GetOpAtForTesting<cc::ClipRectOp>(index); \
ASSERT_NE(nullptr, clip_op); \
EXPECT_EQ(SkRect(r), clip_op->rect); \
} while (false)
#define EXPECT_ROUNDED_CLIP(r, op_buffer, index) \
do { \
const auto* clip_op = \
(op_buffer).GetOpAtForTesting<cc::ClipRRectOp>(index); \
ASSERT_NE(nullptr, clip_op); \
EXPECT_EQ(SkRRect(r), clip_op->rrect); \
} while (false)
PaintChunk::Id DefaultId() {
DEFINE_STATIC_LOCAL(FakeDisplayItemClient, fake_client,
("FakeDisplayItemClient"));
return PaintChunk::Id(fake_client, DisplayItem::kDrawingFirst);
}
class TestChunks {
public:
// Add a paint chunk with a non-empty paint record and given property nodes.
void AddChunk(
const TransformPaintPropertyNodeOrAlias& t,
const ClipPaintPropertyNodeOrAlias& c,
const EffectPaintPropertyNodeOrAlias& e,
const IntRect& bounds = IntRect(0, 0, 100, 100),
const base::Optional<IntRect>& drawable_bounds = base::nullopt) {
auto record = sk_make_sp<PaintRecord>();
record->push<cc::DrawRectOp>(drawable_bounds ? *drawable_bounds : bounds,
cc::PaintFlags());
AddChunk(std::move(record), t, c, e, bounds, drawable_bounds);
}
// Add a paint chunk with a given paint record and property nodes.
void AddChunk(
sk_sp<PaintRecord> record,
const TransformPaintPropertyNodeOrAlias& t,
const ClipPaintPropertyNodeOrAlias& c,
const EffectPaintPropertyNodeOrAlias& e,
const IntRect& bounds = IntRect(0, 0, 100, 100),
const base::Optional<IntRect>& drawable_bounds = base::nullopt) {
auto& items = paint_artifact_->GetDisplayItemList();
auto i = items.size();
items.AllocateAndConstruct<DrawingDisplayItem>(
DefaultId().client, DefaultId().type,
drawable_bounds ? *drawable_bounds : bounds, std::move(record));
auto& chunks = paint_artifact_->PaintChunks();
chunks.emplace_back(i, i + 1, DefaultId(),
PropertyTreeStateOrAlias(t, c, e));
chunks.back().bounds = bounds;
chunks.back().drawable_bounds = drawable_bounds ? *drawable_bounds : bounds;
}
void AddEmptyChunk(const TransformPaintPropertyNode& t,
const ClipPaintPropertyNode& c,
const EffectPaintPropertyNode& e,
const IntRect& bounds = IntRect(0, 0, 100, 100)) {
auto& chunks = paint_artifact_->PaintChunks();
auto i = paint_artifact_->GetDisplayItemList().size();
chunks.emplace_back(i, i, DefaultId(), PropertyTreeState(t, c, e));
chunks.back().bounds = bounds;
}
PaintChunkSubset Build() {
return PaintChunkSubset(std::move(paint_artifact_));
}
private:
scoped_refptr<PaintArtifact> paint_artifact_ =
base::MakeRefCounted<PaintArtifact>();
};
TEST_P(PaintChunksToCcLayerTest, EffectGroupingSimple) {
// This test verifies effects are applied as a group.
auto e1 = CreateOpacityEffect(e0(), 0.5f);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *e1, IntRect(0, 0, 50, 50));
chunks.AddChunk(t0(), c0(), *e1, IntRect(20, 20, 70, 70));
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore})); // </e1>
EXPECT_EFFECT_BOUNDS(0, 0, 90, 90, *output, 0);
}
TEST_P(PaintChunksToCcLayerTest, EffectGroupingNested) {
// This test verifies nested effects are grouped properly.
auto e1 = CreateOpacityEffect(e0(), 0.5f);
auto e2 = CreateOpacityEffect(*e1, 0.5f);
auto e3 = CreateOpacityEffect(*e1, 0.5f);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *e2);
chunks.AddChunk(t0(), c0(), *e3, IntRect(111, 222, 333, 444));
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::SaveLayerAlpha, // <e3>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore, // </e3>
cc::PaintOpType::Restore})); // </e1>
EXPECT_EFFECT_BOUNDS(0, 0, 444, 666, *output, 0);
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 1);
EXPECT_EFFECT_BOUNDS(111, 222, 333, 444, *output, 4);
}
TEST_P(PaintChunksToCcLayerTest, EffectFilterGroupingNestedWithTransforms) {
// This test verifies nested effects with transforms are grouped properly.
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto t2 = Create2DTranslation(*t1, -50, -50);
auto e1 = CreateOpacityEffect(e0(), *t2, &c0(), 0.5);
CompositorFilterOperations filter;
filter.AppendBlurFilter(5);
auto e2 = CreateFilterEffect(*e1, filter);
TestChunks chunks;
chunks.AddChunk(*t2, c0(), *e1, IntRect(0, 0, 50, 50));
chunks.AddChunk(*t1, c0(), *e2, IntRect(20, 20, 70, 70));
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1*t2>
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::SaveLayer, // <e2>
cc::PaintOpType::Save, cc::PaintOpType::Translate, // <t2^-1>
cc::PaintOpType::DrawRecord, // <p2/>
cc::PaintOpType::Restore, // </t2^-1>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore})); // </t1*t2>
EXPECT_TRANSFORM_MATRIX(t1->Matrix() * t2->SlowMatrix(), *output, 1);
// chunk1.bounds + e2(t2^-1(chunk2.bounds))
EXPECT_EFFECT_BOUNDS(0, 0, 155, 155, *output, 2);
// t2^-1(chunk2.bounds)
EXPECT_EFFECT_BOUNDS(70, 70, 70, 70, *output, 4);
// t2^1
EXPECT_TRANSLATE(-t2->Translation2D().Width(), -t2->Translation2D().Height(),
*output, 6);
}
TEST_P(PaintChunksToCcLayerTest, InterleavedClipEffect) {
// This test verifies effects are enclosed by their output clips.
// It is the same as the example made in the class comments of
// ConversionContext.
// Refer to PaintChunksToCcLayer.cpp for detailed explanation.
// (Search "State management example".)
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c2 = CreateClip(*c1, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c3 = CreateClip(*c2, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c4 = CreateClip(*c3, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), t0(), c2.get(), 0.5);
auto e2 = CreateOpacityEffect(*e1, t0(), c4.get(), 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *c2, e0());
chunks.AddChunk(t0(), *c3, e0());
chunks.AddChunk(t0(), *c4, *e2, IntRect(0, 0, 50, 50));
chunks.AddChunk(t0(), *c3, *e1, IntRect(20, 20, 70, 70));
chunks.AddChunk(t0(), *c4, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make(
{cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c1+c2>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c3>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore, // </c3>
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c3+c4>
cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::DrawRecord, // <p2/>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::Restore, // </c3+c4>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c3>
cc::PaintOpType::DrawRecord, // <p3/>
cc::PaintOpType::Restore, // </c3>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c3+c4>
cc::PaintOpType::DrawRecord, // <p4/>
cc::PaintOpType::Restore, // </c3+c4>
cc::PaintOpType::Restore})); // </c1+c2>
EXPECT_EFFECT_BOUNDS(0, 0, 90, 90, *output, 7);
EXPECT_EFFECT_BOUNDS(0, 0, 50, 50, *output, 10);
}
TEST_P(PaintChunksToCcLayerTest, ClipSpaceInversion) {
// This test verifies chunks that have a shallower transform state than
// its clip can still be painted. The infamous CSS corner case:
// <div style="position:absolute; clip:rect(...)">
// <div style="position:fixed;">Clipped but not scroll along.</div>
// </div>
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto c1 = CreateClip(c0(), *t1, FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
TestChunks chunks;
chunks.AddChunk(t0(), *c1, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1
cc::PaintOpType::ClipRect, // c1>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1^-1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </t1^-1>
cc::PaintOpType::Restore})); // </c1 t1>
}
TEST_P(PaintChunksToCcLayerTest, OpacityEffectSpaceInversion) {
// This test verifies chunks that have a shallower transform state than
// its effect can still be painted. The infamous CSS corner case:
// <div style="overflow:scroll">
// <div style="opacity:0.5">
// <div style="position:absolute;">Transparent but not scroll along.</div>
// </div>
// </div>
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto e1 = CreateOpacityEffect(e0(), *t1, &c0(), 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *e1);
chunks.AddChunk(*t1, c0(), *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1>
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1^-1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </t1^-1>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore})); // </t1>
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 2);
EXPECT_TRANSFORM_MATRIX(t1->Matrix(), *output, 1);
EXPECT_TRANSFORM_MATRIX(t1->Matrix().Inverse(), *output, 4);
}
TEST_P(PaintChunksToCcLayerTest, FilterEffectSpaceInversion) {
// This test verifies chunks that have a shallower transform state than
// its effect can still be painted. The infamous CSS corner case:
// <div style="overflow:scroll">
// <div style="filter:blur(1px)">
// <div style="position:absolute;">Filtered but not scroll along.</div>
// </div>
// </div>
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
CompositorFilterOperations filter;
filter.AppendBlurFilter(5);
auto e1 = CreateFilterEffect(e0(), *t1, &c0(), filter);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *e1);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1>
cc::PaintOpType::SaveLayer, // <e1>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1^-1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </t1^-1>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore})); // </t1>
EXPECT_TRANSFORM_MATRIX(t1->Matrix(), *output, 1);
EXPECT_EFFECT_BOUNDS(0, 0, 50, 50, *output, 2);
EXPECT_TRANSFORM_MATRIX(t1->Matrix().Inverse(), *output, 4);
}
TEST_P(PaintChunksToCcLayerTest, NonRootLayerSimple) {
// This test verifies a layer with composited property state does not
// apply properties again internally.
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), 0.5f);
TestChunks chunks;
chunks.AddChunk(*t1, *c1, *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(*t1, *c1, *e1), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({cc::PaintOpType::DrawRecord}));
}
TEST_P(PaintChunksToCcLayerTest, NonRootLayerTransformEscape) {
// This test verifies chunks that have a shallower transform state than the
// layer can still be painted.
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), 0.5f);
TestChunks chunks;
chunks.AddChunk(t0(), *c1, *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(*t1, *c1, *e1), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1^-1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore})); // </t1^-1>
}
TEST_P(PaintChunksToCcLayerTest, EffectWithNoOutputClip) {
// This test verifies effect with no output clip can be correctly processed.
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c2 = CreateClip(*c1, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), t0(), nullptr, 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *c2, *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(t0(), *c1, e0()), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c2>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c2>
cc::PaintOpType::Restore})); // </e1>
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 0);
}
TEST_P(PaintChunksToCcLayerTest,
EffectWithNoOutputClipNestedInDecompositedEffect) {
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), 0.5);
auto e2 = CreateOpacityEffect(*e1, t0(), nullptr, 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *c1, *e2);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c1>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::Restore})); // </e1>
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 0);
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 1);
}
TEST_P(PaintChunksToCcLayerTest,
EffectWithNoOutputClipNestedInCompositedEffect) {
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), 0.5);
auto e2 = CreateOpacityEffect(*e1, t0(), nullptr, 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *c1, *e2);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(t0(), c0(), *e1), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // <c1>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c1>
cc::PaintOpType::Restore})); // </e2>
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 0);
}
TEST_P(PaintChunksToCcLayerTest,
EffectWithNoOutputClipNestedInCompositedEffectAndClip) {
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto e1 = CreateOpacityEffect(e0(), 0.5);
auto e2 = CreateOpacityEffect(*e1, t0(), nullptr, 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *c1, *e2);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(t0(), *c1, *e1), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore})); // </e2>
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 0);
}
TEST_P(PaintChunksToCcLayerTest, VisualRect) {
auto layer_transform =
CreateTransform(t0(), TransformationMatrix().Scale(20));
auto chunk_transform = Create2DTranslation(*layer_transform, 50, 100);
TestChunks chunks;
chunks.AddChunk(*chunk_transform, c0(), e0());
auto cc_list = base::MakeRefCounted<cc::DisplayItemList>(
cc::DisplayItemList::kTopLevelDisplayItemList);
PaintChunksToCcLayer::ConvertInto(
chunks.Build(), PropertyTreeState(*layer_transform, c0(), e0()),
gfx::Vector2dF(100, 200), *cc_list);
EXPECT_EQ(gfx::Rect(-50, -100, 100, 100), cc_list->VisualRectForTesting(4));
EXPECT_THAT(*cc_list->ReleaseAsRecord(),
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, //
cc::PaintOpType::Translate, // <layer_offset>
cc::PaintOpType::Save, //
cc::PaintOpType::Translate, // <layer_transform>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </layer_transform>
cc::PaintOpType::Restore})); // </layer_offset>
}
TEST_P(PaintChunksToCcLayerTest, NoncompositedClipPath) {
auto c1 = CreateClipPathClip(c0(), t0(), FloatRoundedRect(1, 2, 3, 4));
TestChunks chunks;
chunks.AddChunk(t0(), *c1, e0());
auto cc_list = base::MakeRefCounted<cc::DisplayItemList>(
cc::DisplayItemList::kTopLevelDisplayItemList);
PaintChunksToCcLayer::ConvertInto(chunks.Build(), PropertyTreeState::Root(),
gfx::Vector2dF(), *cc_list);
EXPECT_THAT(
*cc_list->ReleaseAsRecord(),
PaintRecordMatcher::Make({cc::PaintOpType::Save, //
cc::PaintOpType::ClipRect, //
cc::PaintOpType::ClipPath, // <clip_path>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore})); // </clip_path>
}
TEST_P(PaintChunksToCcLayerTest, EmptyClipsAreElided) {
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c1c2 = CreateClip(*c1, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c2 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
TestChunks chunks;
chunks.AddChunk(nullptr, t0(), *c1, e0());
chunks.AddChunk(nullptr, t0(), *c1c2, e0());
chunks.AddChunk(nullptr, t0(), *c1c2, e0());
chunks.AddChunk(nullptr, t0(), *c1c2, e0());
chunks.AddChunk(nullptr, t0(), *c1, e0());
// D1
chunks.AddChunk(t0(), *c2, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
// Note that c1 and c1c2 are elided.
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::Save, //
cc::PaintOpType::ClipRect, // <c2>
cc::PaintOpType::DrawRecord, // D1
cc::PaintOpType::Restore, // </c2>
}));
}
TEST_P(PaintChunksToCcLayerTest, NonEmptyClipsAreStored) {
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c1c2 = CreateClip(*c1, t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
auto c2 = CreateClip(c0(), t0(), FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
TestChunks chunks;
chunks.AddChunk(nullptr, t0(), *c1, e0());
chunks.AddChunk(nullptr, t0(), *c1c2, e0());
chunks.AddChunk(nullptr, t0(), *c1c2, e0());
// D1
chunks.AddChunk(t0(), *c1c2, e0());
chunks.AddChunk(nullptr, t0(), *c1, e0());
// D2
chunks.AddChunk(t0(), *c2, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c1+c2>
cc::PaintOpType::DrawRecord, // D1
cc::PaintOpType::Restore, // </c1+c2>
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c2>
cc::PaintOpType::DrawRecord, // D2
cc::PaintOpType::Restore, // </c2>
}));
}
TEST_P(PaintChunksToCcLayerTest, EmptyEffectsAreStored) {
auto e1 = CreateOpacityEffect(e0(), 0.5);
TestChunks chunks;
chunks.AddChunk(nullptr, t0(), c0(), e0());
chunks.AddChunk(nullptr, t0(), c0(), *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::Restore, // </e1>
}));
EXPECT_EFFECT_BOUNDS(0, 0, 100, 100, *output, 0);
}
TEST_P(PaintChunksToCcLayerTest, CombineClips) {
FloatRoundedRect clip_rect(0, 0, 100, 100);
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto c1 = CreateClip(c0(), t0(), clip_rect);
auto c2 = CreateClip(*c1, t0(), clip_rect);
auto c3 = CreateClip(*c2, *t1, clip_rect);
auto c4 = CreateClip(*c3, *t1, clip_rect);
auto c5 = CreateClipPathClip(*c4, *t1, clip_rect);
auto c6 = CreateClip(*c5, *t1, clip_rect);
TestChunks chunks;
chunks.AddChunk(*t1, *c6, e0());
chunks.AddChunk(*t1, *c3, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c1+c2>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1
cc::PaintOpType::ClipRect, cc::PaintOpType::ClipPath, // c3+c4+c5>
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c6>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c6>
cc::PaintOpType::Restore, // </c3+c4+c5 t1>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1
cc::PaintOpType::ClipRect, // c3>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore, // </c3 t1>
cc::PaintOpType::Restore})); // </c1+c2>
}
TEST_P(PaintChunksToCcLayerTest, CombineClipsAcrossTransform) {
FloatRoundedRect clip_rect(0, 0, 100, 100);
auto identity = Create2DTranslation(t0(), 0, 0);
auto non_identity =
CreateTransform(*identity, TransformationMatrix().Scale(2));
auto non_invertible =
CreateTransform(*non_identity, TransformationMatrix().Scale(0));
EXPECT_FALSE(non_invertible->Matrix().IsInvertible());
auto c1 = CreateClip(c0(), t0(), FloatRoundedRect(0, 0, 100, 100));
auto c2 = CreateClip(*c1, *identity, FloatRoundedRect(50, 50, 100, 100));
auto c3 = CreateClip(*c2, *non_identity, FloatRoundedRect(1, 2, 3, 4));
auto c4 = CreateClip(*c3, *non_invertible, FloatRoundedRect(5, 6, 7, 8));
TestChunks chunks;
chunks.AddChunk(*non_invertible, *c4, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
// We combine c1/c2 across |identity|, but not c2/c3 across |non_identity|
// and c3/c4 across |non_invertible|.
EXPECT_THAT(
*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c1+c2>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <non_identity
cc::PaintOpType::ClipRect, // c3>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <non_invertible
cc::PaintOpType::ClipRect, // c4>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c4 non_invertible>
cc::PaintOpType::Restore, // </c3 non_identity>
cc::PaintOpType::Restore})); // </c1+c2>
EXPECT_CLIP(FloatRect(50, 50, 50, 50), *output, 1);
EXPECT_TRANSFORM_MATRIX(non_identity->Matrix(), *output, 3);
EXPECT_CLIP(FloatRect(1, 2, 3, 4), *output, 4);
EXPECT_TRANSFORM_MATRIX(non_invertible->Matrix(), *output, 6);
EXPECT_CLIP(FloatRect(5, 6, 7, 8), *output, 7);
}
TEST_P(PaintChunksToCcLayerTest, CombineClipsWithRoundedRects) {
FloatRoundedRect clip_rect(0, 0, 100, 100);
FloatSize corner(5, 5);
FloatRoundedRect big_rounded_clip_rect(FloatRect(0, 0, 200, 200), corner,
corner, corner, corner);
FloatRoundedRect small_rounded_clip_rect(FloatRect(0, 0, 100, 100), corner,
corner, corner, corner);
auto c1 = CreateClip(c0(), t0(), clip_rect);
auto c2 = CreateClip(*c1, t0(), small_rounded_clip_rect);
auto c3 = CreateClip(*c2, t0(), clip_rect);
auto c4 = CreateClip(*c3, t0(), big_rounded_clip_rect);
auto c5 = CreateClip(*c4, t0(), clip_rect);
auto c6 = CreateClip(*c5, t0(), big_rounded_clip_rect);
auto c7 = CreateClip(*c6, t0(), small_rounded_clip_rect);
TestChunks chunks;
chunks.AddChunk(t0(), *c7, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(
*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save, cc::PaintOpType::ClipRRect, // <c1+c2+c3>
cc::PaintOpType::Save, cc::PaintOpType::ClipRRect, // <c4>
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c5>
cc::PaintOpType::Save, cc::PaintOpType::ClipRRect, // <c6>
cc::PaintOpType::Save, cc::PaintOpType::ClipRRect, // <c7>
cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Restore, // </c7>
cc::PaintOpType::Restore, // </c6>
cc::PaintOpType::Restore, // </c5>
cc::PaintOpType::Restore, // </c4>
cc::PaintOpType::Restore})); // </c1+c2+c3>
EXPECT_ROUNDED_CLIP(small_rounded_clip_rect, *output, 1);
EXPECT_ROUNDED_CLIP(big_rounded_clip_rect, *output, 3);
EXPECT_CLIP(clip_rect.Rect(), *output, 5);
EXPECT_ROUNDED_CLIP(big_rounded_clip_rect, *output, 7);
EXPECT_ROUNDED_CLIP(small_rounded_clip_rect, *output, 9);
}
TEST_P(PaintChunksToCcLayerTest, ChunksSamePropertyTreeState) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto t2 = CreateTransform(*t1, TransformationMatrix().Scale(3.f));
auto c1 = CreateClip(c0(), *t1, FloatRoundedRect(0, 0, 100, 100));
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(*t1, *c1, e0());
chunks.AddChunk(*t1, *c1, e0());
chunks.AddChunk(*t2, *c1, e0());
chunks.AddChunk(*t2, *c1, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::DrawRecord, // <p2/>
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c1>
cc::PaintOpType::DrawRecord, // <p3/>
cc::PaintOpType::DrawRecord, // <p4/>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t2>
cc::PaintOpType::DrawRecord, // <p5/>
cc::PaintOpType::DrawRecord, // <p6/>
cc::PaintOpType::Restore, // </t2>
cc::PaintOpType::Restore, // </c1>
cc::PaintOpType::Restore})); // </t1>
}
TEST_P(PaintChunksToCcLayerTest, NoOpForIdentityTransforms) {
auto t1 = Create2DTranslation(t0(), 0, 0);
auto t2 = Create2DTranslation(*t1, 0, 0);
auto t3 = Create2DTranslation(*t2, 0, 0);
auto c1 = CreateClip(c0(), *t2, FloatRoundedRect(0, 0, 100, 100));
auto c2 = CreateClip(*c1, *t3, FloatRoundedRect(0, 0, 200, 50));
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(*t2, c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(*t1, *c2, e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::DrawRecord, // <p2/>
cc::PaintOpType::DrawRecord, // <p3/>
cc::PaintOpType::DrawRecord, // <p4/>
cc::PaintOpType::DrawRecord, // <p5/>
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // <c1+c2>
cc::PaintOpType::DrawRecord, // <p6/>
cc::PaintOpType::Restore})); // </c1+c2>
}
TEST_P(PaintChunksToCcLayerTest, EffectsWithSameTransform) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2));
auto e1 = CreateOpacityEffect(e0(), *t1, &c0(), 0.1f);
auto e2 = CreateOpacityEffect(e0(), *t1, &c0(), 0.2f);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), *e1);
chunks.AddChunk(*t1, c0(), *e2);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1>
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::DrawRecord, // <p2>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::Restore})); // </t1>
}
TEST_P(PaintChunksToCcLayerTest, NestedEffectsWithSameTransform) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2));
auto e1 = CreateOpacityEffect(e0(), *t1, &c0(), 0.1f);
auto e2 = CreateOpacityEffect(*e1, *t1, &c0(), 0.2f);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), *e1);
chunks.AddChunk(*t1, c0(), *e2);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::DrawRecord, // <p0/>
cc::PaintOpType::Save, cc::PaintOpType::Concat, // <t1>
cc::PaintOpType::SaveLayerAlpha, // <e1>
cc::PaintOpType::DrawRecord, // <p1/>
cc::PaintOpType::SaveLayerAlpha, // <e2>
cc::PaintOpType::DrawRecord, // <p2>
cc::PaintOpType::Restore, // </e2>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore})); // </t1>
}
TEST_P(PaintChunksToCcLayerTest, NoopTransformIsNotEmitted) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2.f));
auto noop_t2 = TransformPaintPropertyNodeAlias::Create(*t1);
auto noop_t3 = TransformPaintPropertyNodeAlias::Create(*noop_t2);
auto t4 = CreateTransform(*noop_t3, TransformationMatrix().Scale(2.f));
auto noop_t5 = TransformPaintPropertyNodeAlias::Create(*t4);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
chunks.AddChunk(*noop_t3, c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
chunks.AddChunk(*t4, c0(), e0());
chunks.AddChunk(*noop_t5, c0(), e0());
chunks.AddChunk(*t4, c0(), e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // draw with t0
cc::PaintOpType::Save, cc::PaintOpType::Concat, // t1
cc::PaintOpType::DrawRecord, // draw with t1
cc::PaintOpType::DrawRecord, // draw with noop_t2
cc::PaintOpType::DrawRecord, // draw with noop_t3
cc::PaintOpType::DrawRecord, // draw with noop_t2
cc::PaintOpType::Restore, // end t1
cc::PaintOpType::Save, cc::PaintOpType::Concat, // t4
cc::PaintOpType::DrawRecord, // draw with t4
cc::PaintOpType::DrawRecord, // draw with noop_t5
cc::PaintOpType::DrawRecord, // draw with t4
cc::PaintOpType::Restore // end t4
}));
}
TEST_P(PaintChunksToCcLayerTest, OnlyNoopTransformIsNotEmitted) {
auto noop_t1 = TransformPaintPropertyNodeAlias::Create(t0());
auto noop_t2 = TransformPaintPropertyNodeAlias::Create(*noop_t1);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*noop_t1, c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({cc::PaintOpType::DrawRecord,
cc::PaintOpType::DrawRecord,
cc::PaintOpType::DrawRecord}));
}
TEST_P(PaintChunksToCcLayerTest, NoopTransformFirstThenBackToParent) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2));
auto noop_t2 = TransformPaintPropertyNodeAlias::Create(*t1);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
chunks.AddChunk(*t1, c0(), e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // t0
cc::PaintOpType::Save,
cc::PaintOpType::Concat, // t1 + noop_t2
cc::PaintOpType::DrawRecord, // draw with above
cc::PaintOpType::DrawRecord, // draw with just t1
cc::PaintOpType::Restore // end t1
}));
}
TEST_P(PaintChunksToCcLayerTest, ClipUndoesNoopTransform) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2));
auto noop_t2 = TransformPaintPropertyNodeAlias::Create(*t1);
auto c1 = CreateClip(c0(), *t1, FloatRoundedRect(0.f, 0.f, 1.f, 1.f));
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
// The clip's local transform is t1, which is the parent of noop_t2.
chunks.AddChunk(*noop_t2, *c1, e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // t0
cc::PaintOpType::Save,
cc::PaintOpType::Concat, // t1 + noop_t2
cc::PaintOpType::DrawRecord, cc::PaintOpType::Save,
cc::PaintOpType::ClipRect, // c1 (with t1 space)
cc::PaintOpType::DrawRecord,
cc::PaintOpType::Restore, // end c1
cc::PaintOpType::Restore // end t1
}));
}
TEST_P(PaintChunksToCcLayerTest, EffectUndoesNoopTransform) {
auto t1 = CreateTransform(t0(), TransformationMatrix().Scale(2));
auto noop_t2 = TransformPaintPropertyNodeAlias::Create(*t1);
auto e1 = CreateOpacityEffect(e0(), *t1, &c0(), 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(*noop_t2, c0(), e0());
// The effects's local transform is t1, which is the parent of noop_t2.
chunks.AddChunk(*noop_t2, c0(), *e1);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // t0
cc::PaintOpType::Save,
cc::PaintOpType::Concat, // t1 + noop_t2
cc::PaintOpType::DrawRecord,
cc::PaintOpType::SaveLayerAlpha, // e1
cc::PaintOpType::DrawRecord,
cc::PaintOpType::Restore, // end e1
cc::PaintOpType::Restore // end t1
}));
}
TEST_P(PaintChunksToCcLayerTest, NoopClipDoesNotEmitItems) {
FloatRoundedRect clip_rect(0.f, 0.f, 1.f, 1.f);
auto c1 = CreateClip(c0(), t0(), clip_rect);
auto noop_c2 = ClipPaintPropertyNodeAlias::Create(*c1);
auto noop_c3 = ClipPaintPropertyNodeAlias::Create(*noop_c2);
auto c4 = CreateClip(*noop_c3, t0(), clip_rect);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(t0(), *c1, e0());
chunks.AddChunk(t0(), *noop_c2, e0());
chunks.AddChunk(t0(), *noop_c3, e0());
chunks.AddChunk(t0(), *c4, e0());
chunks.AddChunk(t0(), *noop_c2, e0());
chunks.AddChunk(t0(), *c1, e0());
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // c0
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // c1
cc::PaintOpType::DrawRecord, // draw with c1
cc::PaintOpType::DrawRecord, // draw with noop_c2
cc::PaintOpType::DrawRecord, // draw_with noop_c3
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // c4
cc::PaintOpType::DrawRecord, // draw with c4
cc::PaintOpType::Restore, // end c4
cc::PaintOpType::DrawRecord, // draw with noop_c2
cc::PaintOpType::DrawRecord, // draw with c1
cc::PaintOpType::Restore // end noop_c2 (or c1)
}));
}
TEST_P(PaintChunksToCcLayerTest, EffectUndoesNoopClip) {
FloatRoundedRect clip_rect(0.f, 0.f, 1.f, 1.f);
auto c1 = CreateClip(c0(), t0(), clip_rect);
auto noop_c2 = ClipPaintPropertyNodeAlias::Create(*c1);
auto e1 = CreateOpacityEffect(e0(), t0(), c1.get(), 0.5);
TestChunks chunks;
chunks.AddChunk(t0(), *noop_c2, e0());
chunks.AddChunk(t0(), *noop_c2, *e1);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({
cc::PaintOpType::Save, cc::PaintOpType::ClipRect, // noop_c2
cc::PaintOpType::DrawRecord, // draw with noop_c2
cc::PaintOpType::SaveLayerAlpha, // e1
cc::PaintOpType::DrawRecord, // draw with e1
cc::PaintOpType::Restore, // end e1
cc::PaintOpType::Restore // end noop_c2
}));
}
// These tests are testing error recovery path that are only used in
// release builds. A DCHECK'd build will trap instead.
#if !DCHECK_IS_ON()
TEST_P(PaintChunksToCcLayerTest, SPv1ChunkEscapeLayerClipFailSafe) {
ScopedCompositeAfterPaintForTest cap_disabler(false);
// This test verifies the fail-safe path correctly recovers from a malformed
// chunk that escaped its layer's clip.
FloatRoundedRect clip_rect(0.f, 0.f, 1.f, 1.f);
auto c1 = CreateClip(c0(), t0(), clip_rect);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(t0(), *c1, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(t0(), *c1, e0()), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
// We don't care about the exact output as long as it didn't crash.
}
TEST_P(PaintChunksToCcLayerTest, SPv1ChunkEscapeEffectClipFailSafe) {
ScopedCompositeAfterPaintForTest cap_disabler(false);
// This test verifies the fail-safe path correctly recovers from a malformed
// chunk that escaped its effect's clip.
FloatRoundedRect clip_rect(0.f, 0.f, 1.f, 1.f);
auto c1 = CreateClip(c0(), t0(), clip_rect);
CompositorFilterOperations filter;
filter.AppendBlurFilter(5);
auto e1 = CreateFilterEffect(e0(), t0(), c1.get(), std::move(filter));
TestChunks chunks;
chunks.AddChunk(t0(), *c1, *e1);
chunks.AddChunk(t0(), c0(), *e1);
chunks.AddChunk(t0(), *c1, *e1);
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
// We don't care about the exact output as long as it didn't crash.
}
TEST_P(PaintChunksToCcLayerTest, SPv1ChunkEscapeLayerClipDoubleFault) {
ScopedCompositeAfterPaintForTest cap_disabler(false);
// This test verifies the fail-safe path correctly recovers from a series of
// malformed chunks that escaped their layer's clip.
FloatRoundedRect clip_rect(0.f, 0.f, 1.f, 1.f);
auto c1 = CreateClip(c0(), t0(), clip_rect);
auto c2 = CreateClip(c0(), t0(), clip_rect);
auto c3 = CreateClip(c0(), t0(), clip_rect);
TestChunks chunks;
chunks.AddChunk(t0(), *c2, e0());
chunks.AddChunk(t0(), *c3, e0());
sk_sp<PaintRecord> output =
PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState(t0(), *c1, e0()), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
// We don't care about the exact output as long as it didn't crash.
}
#endif
TEST_P(PaintChunksToCcLayerTest, NoopEffectDoesNotEmitItems) {
auto e1 = CreateOpacityEffect(e0(), 0.5f);
auto noop_e2 = EffectPaintPropertyNodeAlias::Create(*e1);
auto noop_e3 = EffectPaintPropertyNodeAlias::Create(*noop_e2);
auto e4 = CreateOpacityEffect(*noop_e3, 0.5f);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), e0());
chunks.AddChunk(t0(), c0(), *e1);
chunks.AddChunk(t0(), c0(), *noop_e2);
chunks.AddChunk(t0(), c0(), *noop_e3);
chunks.AddChunk(t0(), c0(), *e4);
chunks.AddChunk(t0(), c0(), *noop_e2);
chunks.AddChunk(t0(), c0(), *e1);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord, // e0
cc::PaintOpType::SaveLayerAlpha, // e1
cc::PaintOpType::DrawRecord, // draw with e1
cc::PaintOpType::DrawRecord, // draw with noop_e2
cc::PaintOpType::DrawRecord, // draw_with noop_e3
cc::PaintOpType::SaveLayerAlpha, // e4
cc::PaintOpType::DrawRecord, // draw with e4
cc::PaintOpType::Restore, // end e4
cc::PaintOpType::DrawRecord, // draw with noop_e2
cc::PaintOpType::DrawRecord, // draw with e1
cc::PaintOpType::Restore // end noop_e2 (or e1)
}));
}
TEST_P(PaintChunksToCcLayerTest, AllowChunkEscapeLayerNoopEffects) {
// This test doesn't apply to CAP.
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return;
auto e1 = CreateOpacityEffect(e0(), 0.5f);
auto noop_e2 = CreateOpacityEffect(*e1, 1.0f);
auto noop_e3 = CreateOpacityEffect(*noop_e2, 1.0f);
auto e4 = CreateOpacityEffect(*e1, 0.5f);
PropertyTreeState layer_state(t0(), c0(), *noop_e3);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *noop_e3);
chunks.AddChunk(t0(), c0(), *e4);
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), layer_state, gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make({
cc::PaintOpType::DrawRecord,
cc::PaintOpType::SaveLayerAlpha, // e4
cc::PaintOpType::DrawRecord,
cc::PaintOpType::Restore, // end e4
}));
}
TEST_P(PaintChunksToCcLayerTest, EmptyChunkRect) {
CompositorFilterOperations filter;
filter.AppendBlurFilter(5);
auto e1 = CreateFilterEffect(e0(), t0(), &c0(), filter);
TestChunks chunks;
chunks.AddChunk(nullptr, t0(), c0(), *e1, {0, 0, 0, 0});
auto output = PaintChunksToCcLayer::Convert(
chunks.Build(), PropertyTreeState::Root(), gfx::Vector2dF(),
cc::DisplayItemList::kToBeReleasedAsPaintOpBuffer)
->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make({cc::PaintOpType::SaveLayer, // <e1>
cc::PaintOpType::Restore})); // </e1>
EXPECT_EFFECT_BOUNDS(0, 0, 0, 0, *output, 0);
}
static sk_sp<cc::PaintFilter> MakeFilter(FloatRect bounds) {
PaintFilter::CropRect rect(bounds);
return sk_make_sp<ColorFilterPaintFilter>(
SkColorFilters::Blend(SK_ColorBLUE, SkBlendMode::kSrc), nullptr, &rect);
}
TEST_P(PaintChunksToCcLayerTest, ReferenceFilterOnEmptyChunk) {
CompositorFilterOperations filter;
filter.AppendReferenceFilter(MakeFilter(FloatRect(12, 26, 93, 84)));
filter.SetReferenceBox(FloatRect(11, 22, 33, 44));
ASSERT_TRUE(filter.HasReferenceFilter());
auto e1 = CreateFilterEffect(e0(), t0(), &c0(), filter);
TestChunks chunks;
chunks.AddEmptyChunk(t0(), c0(), *e1, IntRect(0, 0, 200, 300));
auto cc_list = base::MakeRefCounted<cc::DisplayItemList>(
cc::DisplayItemList::kTopLevelDisplayItemList);
PaintChunksToCcLayer::ConvertInto(chunks.Build(), PropertyTreeState::Root(),
gfx::Vector2dF(5, 10), *cc_list);
ASSERT_EQ(5u, cc_list->TotalOpCount());
// (7 16) is (12, 26) - layer_offset.
gfx::Rect expected_visual_rect(7, 16, 93, 84);
for (size_t i = 0; i < cc_list->TotalOpCount(); i++) {
SCOPED_TRACE(testing::Message() << "Visual rect of op " << i);
EXPECT_EQ(expected_visual_rect, cc_list->VisualRectForTesting(i));
}
auto output = cc_list->ReleaseAsRecord();
EXPECT_THAT(*output, PaintRecordMatcher::Make(
{cc::PaintOpType::Save,
cc::PaintOpType::Translate, // layer offset
cc::PaintOpType::SaveLayer, // <e1>
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore}));
EXPECT_EFFECT_BOUNDS(12, 26, 93, 84, *output, 2);
}
TEST_P(PaintChunksToCcLayerTest, ReferenceFilterOnChunkWithDrawingDisplayItem) {
CompositorFilterOperations filter;
filter.AppendReferenceFilter(MakeFilter(FloatRect(7, 16, 93, 84)));
filter.SetReferenceBox(FloatRect(11, 22, 33, 44));
ASSERT_TRUE(filter.HasReferenceFilter());
auto e1 = CreateFilterEffect(e0(), t0(), &c0(), filter);
TestChunks chunks;
chunks.AddChunk(t0(), c0(), *e1, IntRect(5, 10, 200, 300),
IntRect(10, 15, 20, 30));
auto cc_list = base::MakeRefCounted<cc::DisplayItemList>(
cc::DisplayItemList::kTopLevelDisplayItemList);
PaintChunksToCcLayer::ConvertInto(chunks.Build(), PropertyTreeState::Root(),
gfx::Vector2dF(5, 10), *cc_list);
ASSERT_EQ(7u, cc_list->TotalOpCount());
// This is the visual rect for all filter related paint operations, which is
// the union of the draw record and the output bounds of the filter with empty
// input in the layer's space. This is also the rect that the chunk bounds map
// to via MapVisualRect since the filter does not actually use the source.
gfx::Rect expected_filter_visual_rect(2, 6, 93, 84);
// TotalOpCount() - 1 because the DrawRecord op has a sub operation.
for (size_t i = 0; i < cc_list->TotalOpCount() - 1; i++) {
SCOPED_TRACE(testing::Message() << "Visual rect of op " << i);
EXPECT_EQ(expected_filter_visual_rect, cc_list->VisualRectForTesting(i));
}
auto output = cc_list->ReleaseAsRecord();
EXPECT_THAT(*output,
PaintRecordMatcher::Make(
{cc::PaintOpType::Save,
cc::PaintOpType::Translate, // layer offset
cc::PaintOpType::SaveLayer, // <e1>
cc::PaintOpType::DrawRecord, // the DrawingDisplayItem
cc::PaintOpType::Restore, // </e1>
cc::PaintOpType::Restore}));
// The effect bounds are the union of the chunk's drawable_bounds and the
// output bounds of the filter with empty input in the filter's space.
EXPECT_EFFECT_BOUNDS(7, 15, 93, 85, *output, 2);
}
} // namespace
} // namespace blink