blob: d0d49c6dc3aff59e7f7c6b6db92698900d8f2648 [file] [log] [blame]
// Copyright 2018 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 "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "cc/layers/picture_layer.h"
#include "cc/layers/recording_source.h"
#include "cc/layers/surface_layer.h"
#include "cc/trees/compositor_commit_data.h"
#include "cc/trees/effect_node.h"
#include "cc/trees/layer_tree_host.h"
#include "cc/trees/scroll_node.h"
#include "cc/trees/transform_node.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/paint/compositing/composited_layer_mapping.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/svg_names.h"
#include "third_party/blink/renderer/core/testing/fake_remote_frame_host.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
#include "third_party/blink/renderer/platform/testing/find_cc_layer.h"
#include "third_party/blink/renderer/platform/testing/paint_test_configurations.h"
namespace blink {
// Tests the integration between blink and cc where a layer list is sent to cc.
class CompositingTest : public PaintTestConfigurations, public testing::Test {
public:
static void ConfigureCompositingWebView(WebSettings* settings) {
settings->SetPreferCompositingToLCDTextEnabled(true);
}
void SetUp() override {
web_view_helper_ = std::make_unique<frame_test_helpers::WebViewHelper>();
web_view_helper_->Initialize(nullptr, nullptr,
&ConfigureCompositingWebView);
web_view_helper_->Resize(gfx::Size(200, 200));
}
void TearDown() override { web_view_helper_.reset(); }
// Both sets the inner html and runs the document lifecycle.
void InitializeWithHTML(LocalFrame& frame, const String& html_content) {
frame.GetDocument()->body()->setInnerHTML(html_content);
frame.GetDocument()->View()->UpdateAllLifecyclePhasesForTest();
}
WebLocalFrame* LocalMainFrame() { return web_view_helper_->LocalMainFrame(); }
LocalFrameView* GetLocalFrameView() {
return web_view_helper_->LocalMainFrame()->GetFrameView();
}
WebViewImpl* WebView() { return web_view_helper_->GetWebView(); }
const cc::Layer* RootCcLayer() {
return paint_artifact_compositor()->RootLayer();
}
const cc::Layer* CcLayerByDOMElementId(const char* id) {
auto layers = CcLayersByDOMElementId(RootCcLayer(), id);
return layers.IsEmpty() ? nullptr : layers[0];
}
cc::LayerTreeHost* LayerTreeHost() {
return web_view_helper_->LocalMainFrame()
->FrameWidgetImpl()
->LayerTreeHostForTesting();
}
Element* GetElementById(const AtomicString& id) {
WebLocalFrameImpl* frame = web_view_helper_->LocalMainFrame();
return frame->GetFrame()->GetDocument()->getElementById(id);
}
LayoutObject* GetLayoutObjectById(const AtomicString& id) {
return GetElementById(id)->GetLayoutObject();
}
void UpdateAllLifecyclePhases() {
WebView()->MainFrameWidget()->UpdateAllLifecyclePhases(
DocumentUpdateReason::kTest);
}
cc::PropertyTrees* GetPropertyTrees() {
return LayerTreeHost()->property_trees();
}
cc::TransformNode* GetTransformNode(const cc::Layer* layer) {
return GetPropertyTrees()->transform_tree.Node(
layer->transform_tree_index());
}
PaintArtifactCompositor* paint_artifact_compositor() {
return GetLocalFrameView()->GetPaintArtifactCompositor();
}
private:
std::unique_ptr<frame_test_helpers::WebViewHelper> web_view_helper_;
};
INSTANTIATE_PAINT_TEST_SUITE_P(CompositingTest);
TEST_P(CompositingTest, DisableAndEnableAcceleratedCompositing) {
UpdateAllLifecyclePhases();
auto* settings = GetLocalFrameView()->GetFrame().GetSettings();
size_t num_layers = RootCcLayer()->children().size();
EXPECT_GT(num_layers, 1u);
settings->SetAcceleratedCompositingEnabled(false);
UpdateAllLifecyclePhases();
EXPECT_FALSE(paint_artifact_compositor());
settings->SetAcceleratedCompositingEnabled(true);
UpdateAllLifecyclePhases();
EXPECT_EQ(num_layers, RootCcLayer()->children().size());
}
TEST_P(CompositingTest, DidScrollCallbackAfterScrollableAreaChanges) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(),
"<style>"
" #scrollable {"
" height: 100px;"
" width: 100px;"
" overflow: scroll;"
" will-change: transform;"
" }"
" #forceScroll { height: 120px; width: 50px; }"
"</style>"
"<div id='scrollable'>"
" <div id='forceScroll'></div>"
"</div>");
UpdateAllLifecyclePhases();
Document* document = WebView()->MainFrameImpl()->GetFrame()->GetDocument();
Element* scrollable = document->getElementById("scrollable");
auto* scrollable_area = scrollable->GetLayoutBox()->GetScrollableArea();
EXPECT_NE(nullptr, scrollable_area);
CompositorElementId scroll_element_id = scrollable_area->GetScrollElementId();
const auto* overflow_scroll_layer =
CcLayerByCcElementId(RootCcLayer(), scroll_element_id);
const auto* scroll_node =
RootCcLayer()
->layer_tree_host()
->property_trees()
->scroll_tree.FindNodeFromElementId(scroll_element_id);
EXPECT_TRUE(scroll_node->scrollable);
EXPECT_EQ(scroll_node->container_bounds, gfx::Size(100, 100));
// Ensure a synthetic impl-side scroll offset propagates to the scrollable
// area using the DidScroll callback.
EXPECT_EQ(ScrollOffset(), scrollable_area->GetScrollOffset());
cc::CompositorCommitData commit_data;
commit_data.scrolls.push_back(
{scroll_element_id, gfx::ScrollOffset(0, 1), absl::nullopt});
overflow_scroll_layer->layer_tree_host()->ApplyCompositorChanges(
&commit_data);
UpdateAllLifecyclePhases();
EXPECT_EQ(ScrollOffset(0, 1), scrollable_area->GetScrollOffset());
// Make the scrollable area non-scrollable.
scrollable->setAttribute(html_names::kStyleAttr, "overflow: visible");
// Update layout without updating compositing state.
LocalMainFrame()->ExecuteScript(
WebScriptSource("var forceLayoutFromScript = scrollable.offsetTop;"));
EXPECT_EQ(document->Lifecycle().GetState(), DocumentLifecycle::kLayoutClean);
EXPECT_EQ(nullptr, scrollable->GetLayoutBox()->GetScrollableArea());
// The web scroll layer has not been deleted yet and we should be able to
// apply impl-side offsets without crashing.
ASSERT_EQ(overflow_scroll_layer,
CcLayerByCcElementId(RootCcLayer(), scroll_element_id));
commit_data.scrolls[0] = {scroll_element_id, gfx::ScrollOffset(0, 1),
absl::nullopt};
overflow_scroll_layer->layer_tree_host()->ApplyCompositorChanges(
&commit_data);
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByCcElementId(RootCcLayer(), scroll_element_id));
}
TEST_P(CompositingTest, FrameViewScroll) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(),
"<style>"
" #forceScroll {"
" height: 2000px;"
" width: 100px;"
" }"
"</style>"
"<div id='forceScroll'></div>");
UpdateAllLifecyclePhases();
auto* scrollable_area = GetLocalFrameView()->LayoutViewport();
EXPECT_NE(nullptr, scrollable_area);
const auto* scroll_node = RootCcLayer()
->layer_tree_host()
->property_trees()
->scroll_tree.FindNodeFromElementId(
scrollable_area->GetScrollElementId());
ASSERT_TRUE(scroll_node);
EXPECT_TRUE(scroll_node->scrollable);
// Ensure a synthetic impl-side scroll offset propagates to the scrollable
// area using the DidScroll callback.
EXPECT_EQ(ScrollOffset(), scrollable_area->GetScrollOffset());
cc::CompositorCommitData commit_data;
commit_data.scrolls.push_back({scrollable_area->GetScrollElementId(),
gfx::ScrollOffset(0, 1), absl::nullopt});
RootCcLayer()->layer_tree_host()->ApplyCompositorChanges(&commit_data);
UpdateAllLifecyclePhases();
EXPECT_EQ(ScrollOffset(0, 1), scrollable_area->GetScrollOffset());
}
TEST_P(CompositingTest, WillChangeTransformHint) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<style>
#willChange {
width: 100px;
height: 100px;
will-change: transform;
background: blue;
}
</style>
<div id="willChange"></div>
)HTML");
UpdateAllLifecyclePhases();
auto* layer = CcLayerByDOMElementId("willChange");
auto* transform_node = GetTransformNode(layer);
EXPECT_TRUE(transform_node->will_change_transform);
}
TEST_P(CompositingTest, WillChangeTransformHintInSVG) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<style>
#willChange {
width: 100px;
height: 100px;
will-change: transform;
}
</style>
<svg width="200" height="200">
<rect id="willChange" fill="blue"></rect>
</svg>
)HTML");
UpdateAllLifecyclePhases();
auto* layer = CcLayerByDOMElementId("willChange");
auto* transform_node = GetTransformNode(layer);
// For now will-change:transform triggers compositing for SVG, but we don't
// pass the flag to cc to ensure raster quality.
EXPECT_FALSE(transform_node->will_change_transform);
}
TEST_P(CompositingTest, Compositing3DTransformOnSVGModelObject) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<svg width="200" height="200">
<rect id="target" fill="blue" width="100" height="100"></rect>
</svg>
)HTML");
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Adding a 3D transform should trigger compositing.
auto* target_element = GetElementById("target");
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate3d(0, 0, 1px)");
UpdateAllLifecyclePhases();
// |HasTransformRelatedProperty| is used in |CompositingReasonsFor3DTransform|
// and must be set correctly.
ASSERT_TRUE(GetLayoutObjectById("target")->HasTransformRelatedProperty());
EXPECT_TRUE(CcLayerByDOMElementId("target"));
// Removing a 3D transform removes the compositing trigger.
target_element->setAttribute(html_names::kStyleAttr, "transform: none");
UpdateAllLifecyclePhases();
// |HasTransformRelatedProperty| is used in |CompositingReasonsFor3DTransform|
// and must be set correctly.
ASSERT_FALSE(GetLayoutObjectById("target")->HasTransformRelatedProperty());
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Adding a 2D transform should not trigger compositing.
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate(1px, 0)");
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Switching from a 2D to a 3D transform should trigger compositing.
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate3d(0, 0, 1px)");
UpdateAllLifecyclePhases();
EXPECT_TRUE(CcLayerByDOMElementId("target"));
}
TEST_P(CompositingTest, Compositing3DTransformOnSVGBlock) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<svg width="200" height="200">
<text id="target" x="50" y="50">text</text>
</svg>
)HTML");
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Adding a 3D transform should trigger compositing.
auto* target_element = GetElementById("target");
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate3d(0, 0, 1px)");
UpdateAllLifecyclePhases();
// |HasTransformRelatedProperty| is used in |CompositingReasonsFor3DTransform|
// and must be set correctly.
ASSERT_TRUE(GetLayoutObjectById("target")->HasTransformRelatedProperty());
EXPECT_TRUE(CcLayerByDOMElementId("target"));
// Removing a 3D transform removes the compositing trigger.
target_element->setAttribute(html_names::kStyleAttr, "transform: none");
UpdateAllLifecyclePhases();
// |HasTransformRelatedProperty| is used in |CompositingReasonsFor3DTransform|
// and must be set correctly.
ASSERT_FALSE(GetLayoutObjectById("target")->HasTransformRelatedProperty());
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Adding a 2D transform should not trigger compositing.
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate(1px, 0)");
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByDOMElementId("target"));
// Switching from a 2D to a 3D transform should trigger compositing.
target_element->setAttribute(html_names::kStyleAttr,
"transform: translate3d(0, 0, 1px)");
UpdateAllLifecyclePhases();
EXPECT_TRUE(CcLayerByDOMElementId("target"));
}
// Inlines do not support the transform property and should not be composited
// due to 3D transforms.
TEST_P(CompositingTest, NotCompositing3DTransformOnSVGInline) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<svg width="200" height="200">
<text x="50" y="50">
text
<tspan id="inline">tspan</tspan>
</text>
</svg>
)HTML");
UpdateAllLifecyclePhases();
EXPECT_FALSE(CcLayerByDOMElementId("inline"));
// Adding a 3D transform to an inline should not trigger compositing.
auto* inline_element = GetElementById("inline");
inline_element->setAttribute(html_names::kStyleAttr,
"transform: translate3d(0, 0, 1px)");
UpdateAllLifecyclePhases();
// |HasTransformRelatedProperty| is used in |CompositingReasonsFor3DTransform|
// and must be set correctly.
ASSERT_FALSE(GetLayoutObjectById("inline")->HasTransformRelatedProperty());
EXPECT_FALSE(CcLayerByDOMElementId("inline"));
}
TEST_P(CompositingTest, PaintPropertiesWhenCompositingSVG) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<style>
#ancestor {
opacity: 0.9;
}
#svg {
opacity: 0.8;
}
#rect {
width: 100px;
height: 100px;
will-change: transform;
opacity: 0.7;
}
</style>
<div id="ancestor">
<svg id="svg" width="200" height="200">
<rect width="10" height="10" fill="red"></rect>
<rect id="rect" fill="blue" stroke-width="1" stroke="black"></rect>
</svg>
</div>
)HTML");
UpdateAllLifecyclePhases();
auto* ancestor = CcLayerByDOMElementId("ancestor");
auto* ancestor_effect_node =
GetPropertyTrees()->effect_tree.Node(ancestor->effect_tree_index());
EXPECT_EQ(ancestor_effect_node->opacity, 0.9f);
auto* svg_root = CcLayerByDOMElementId("svg");
auto* svg_root_effect_node =
GetPropertyTrees()->effect_tree.Node(svg_root->effect_tree_index());
EXPECT_EQ(svg_root_effect_node->opacity, 0.8f);
EXPECT_EQ(svg_root_effect_node->parent_id, ancestor_effect_node->id);
auto* rect = CcLayerByDOMElementId("rect");
auto* rect_filter_node =
GetPropertyTrees()->effect_tree.Node(rect->effect_tree_index());
EXPECT_EQ(rect_filter_node->opacity, 1);
auto* rect_effect_node =
GetPropertyTrees()->effect_tree.Node(rect_filter_node->parent_id);
EXPECT_EQ(rect_effect_node->opacity, 0.7f);
EXPECT_EQ(rect_effect_node->parent_id, svg_root_effect_node->id);
}
TEST_P(CompositingTest, BackgroundColorInScrollingContentsLayer) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<style>
html {
background-color: rgb(10, 20, 30);
}
#scroller {
will-change: transform;
overflow: scroll;
height: 100px;
width: 100px;
background-color: rgb(30, 40, 50);
}
.spacer {
height: 1000px;
}
</style>
<div id="scroller">
<div class="spacer"></div>
</div>
<div class="spacer"></div>
)HTML");
UpdateAllLifecyclePhases();
LayoutView* layout_view = GetLocalFrameView()->GetLayoutView();
Element* scroller = GetElementById("scroller");
LayoutBox* scroller_box = scroller->GetLayoutBox();
ASSERT_TRUE(layout_view->GetBackgroundPaintLocation() ==
kBackgroundPaintInScrollingContents);
ASSERT_TRUE(scroller_box->GetBackgroundPaintLocation() ==
kBackgroundPaintInScrollingContents);
// The root layer and root scrolling contents layer get background_color by
// blending the CSS background-color of the <html> element with
// LocalFrameView::BaseBackgroundColor(), which is white by default.
auto* layer = CcLayersByName(RootCcLayer(), "LayoutView #document")[0];
SkColor expected_color = SkColorSetRGB(10, 20, 30);
EXPECT_EQ(layer->background_color(), SK_ColorTRANSPARENT);
auto* scrollable_area = GetLocalFrameView()->LayoutViewport();
layer = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(), scrollable_area->GetScrollElementId());
EXPECT_EQ(layer->background_color(), expected_color);
// Non-root layers set background_color based on the CSS background color of
// the layer-defining element.
expected_color = SkColorSetRGB(30, 40, 50);
layer = CcLayerByDOMElementId("scroller");
EXPECT_EQ(layer->background_color(), SK_ColorTRANSPARENT);
scrollable_area = scroller_box->GetScrollableArea();
layer = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(), scrollable_area->GetScrollElementId());
EXPECT_EQ(layer->background_color(), expected_color);
}
TEST_P(CompositingTest, BackgroundColorInGraphicsLayer) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<style>
html {
background-image: linear-gradient(rgb(10, 20, 30), rgb(60, 70, 80));
background-attachment: fixed;
}
#scroller {
will-change: transform;
overflow: scroll;
height: 100px;
width: 100px;
background-color: rgba(30, 40, 50, .6);
background-clip: content-box;
background-attachment: scroll;
padding: 1px;
}
.spacer {
height: 1000px;
}
</style>
<div id="scroller">
<div class="spacer"></div>
</div>
<div class="spacer"></div>
)HTML");
UpdateAllLifecyclePhases();
LayoutView* layout_view = GetLocalFrameView()->GetLayoutView();
Element* scroller = GetElementById("scroller");
LayoutBox* scroller_box = scroller->GetLayoutBox();
ASSERT_TRUE(layout_view->GetBackgroundPaintLocation() ==
kBackgroundPaintInGraphicsLayer);
ASSERT_TRUE(scroller_box->GetBackgroundPaintLocation() ==
kBackgroundPaintInGraphicsLayer);
// The root layer gets background_color by blending the CSS background-color
// of the <html> element with LocalFrameView::BaseBackgroundColor(), which is
// white by default. In this case, because the background is a gradient, it
// will blend transparent with white, resulting in white. Because the
// background is painted into the root graphics layer, the root scrolling
// contents layer should not checkerboard, so its background color should be
// transparent.
auto* layer = CcLayersByName(RootCcLayer(), "LayoutView #document")[0];
EXPECT_EQ(layer->background_color(), SK_ColorWHITE);
auto* scrollable_area = GetLocalFrameView()->LayoutViewport();
layer = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(), scrollable_area->GetScrollElementId());
EXPECT_EQ(layer->background_color(), SK_ColorTRANSPARENT);
EXPECT_EQ(layer->SafeOpaqueBackgroundColor(), SK_ColorTRANSPARENT);
// Non-root layers set background_color based on the CSS background color of
// the layer-defining element.
SkColor expected_color = SkColorSetARGB(roundf(255. * 0.6), 30, 40, 50);
layer = CcLayerByDOMElementId("scroller");
EXPECT_EQ(layer->background_color(), expected_color);
scrollable_area = scroller_box->GetScrollableArea();
layer = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(), scrollable_area->GetScrollElementId());
EXPECT_EQ(layer->background_color(), SK_ColorTRANSPARENT);
EXPECT_EQ(layer->SafeOpaqueBackgroundColor(), SK_ColorTRANSPARENT);
}
TEST_P(CompositingTest, ContainPaintLayerBounds) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<div id="target" style="will-change: transform; contain: paint;
width: 200px; height: 100px">
<div style="width: 300px; height: 400px"></div>
</div>
)HTML");
UpdateAllLifecyclePhases();
auto* layer = CcLayersByDOMElementId(RootCcLayer(), "target")[0];
ASSERT_TRUE(layer);
EXPECT_EQ(gfx::Size(200, 100), layer->bounds());
}
TEST_P(CompositingTest, SVGForeignObjectDirectlyCompositedContainer) {
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return;
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<!doctype html>
<div id="container" style="backface-visibility: hidden">
<svg>
<foreignObject id="foreign">
<div id="child" style="position: relative"></div>
</foreignObject>
</svg>
</body>
)HTML");
UpdateAllLifecyclePhases();
// PaintInvalidator should use the same directly_composited_container during
// PrePaint and should not fail DCHECK.
auto* container = GetLayoutObjectById("container");
EXPECT_FALSE(container->IsStackingContext());
EXPECT_EQ(container,
&GetLayoutObjectById("foreign")->DirectlyCompositableContainer());
EXPECT_EQ(container,
&GetLayoutObjectById("child")->DirectlyCompositableContainer());
}
class CompositingSimTest : public PaintTestConfigurations, public SimTest {
public:
void InitializeWithHTML(const String& html) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(html);
UpdateAllLifecyclePhases();
DCHECK(paint_artifact_compositor());
}
const cc::Layer* RootCcLayer() {
return paint_artifact_compositor()->RootLayer();
}
const cc::Layer* CcLayerByDOMElementId(const char* id) {
auto layers = CcLayersByDOMElementId(RootCcLayer(), id);
return layers.IsEmpty() ? nullptr : layers[0];
}
const cc::Layer* CcLayerByOwnerNodeId(Node* node) {
DOMNodeId id = DOMNodeIds::IdForNode(node);
for (auto& layer : RootCcLayer()->children()) {
if (layer->debug_info() && layer->debug_info()->owner_node_id == id)
return layer.get();
}
return nullptr;
}
Element* GetElementById(const AtomicString& id) {
return MainFrame().GetFrame()->GetDocument()->getElementById(id);
}
void UpdateAllLifecyclePhases() {
WebView().MainFrameWidget()->UpdateAllLifecyclePhases(
DocumentUpdateReason::kTest);
}
void UpdateAllLifecyclePhasesExceptPaint() {
WebView().MainFrameWidget()->UpdateLifecycle(WebLifecycleUpdate::kPrePaint,
DocumentUpdateReason::kTest);
}
cc::PropertyTrees* GetPropertyTrees() {
return Compositor().LayerTreeHost()->property_trees();
}
cc::TransformNode* GetTransformNode(const cc::Layer* layer) {
return GetPropertyTrees()->transform_tree.Node(
layer->transform_tree_index());
}
cc::EffectNode* GetEffectNode(const cc::Layer* layer) {
return GetPropertyTrees()->effect_tree.Node(layer->effect_tree_index());
}
PaintArtifactCompositor* paint_artifact_compositor() {
return MainFrame().GetFrameView()->GetPaintArtifactCompositor();
}
private:
void SetUp() override {
SimTest::SetUp();
// Ensure a non-empty size so painting does not early-out.
WebView().Resize(gfx::Size(800, 600));
}
};
INSTANTIATE_PAINT_TEST_SUITE_P(CompositingSimTest);
TEST_P(CompositingSimTest, LayerUpdatesDoNotInvalidateEarlierLayers) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
div {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='a'></div>
<div id='b'></div>
)HTML");
Compositor().BeginFrame();
auto* a_layer = CcLayerByDOMElementId("a");
auto* b_element = GetElementById("b");
auto* b_layer = CcLayerByDOMElementId("b");
// Initially, neither a nor b should have a layer that should push properties.
cc::LayerTreeHost& host = *Compositor().LayerTreeHost();
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(b_layer));
// Modifying b should only cause the b layer to need to push properties.
b_element->setAttribute(html_names::kStyleAttr, "opacity: 0.2");
UpdateAllLifecyclePhases();
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_TRUE(host.LayersThatShouldPushProperties().count(b_layer));
// After a frame, no layers should need to push properties again.
Compositor().BeginFrame();
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(b_layer));
}
TEST_P(CompositingSimTest, LayerUpdatesDoNotInvalidateLaterLayers) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
div {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='a'></div>
<div id='b' style='opacity: 0.2;'></div>
<div id='c'></div>
)HTML");
Compositor().BeginFrame();
auto* a_element = GetElementById("a");
auto* a_layer = CcLayerByDOMElementId("a");
auto* b_element = GetElementById("b");
auto* b_layer = CcLayerByDOMElementId("b");
auto* c_layer = CcLayerByDOMElementId("c");
// Initially, no layer should need to push properties.
cc::LayerTreeHost& host = *Compositor().LayerTreeHost();
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(b_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(c_layer));
// Modifying a and b (adding opacity to a and removing opacity from b) should
// not cause the c layer to push properties.
a_element->setAttribute(html_names::kStyleAttr, "opacity: 0.3");
b_element->setAttribute(html_names::kStyleAttr, "");
UpdateAllLifecyclePhases();
EXPECT_TRUE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_TRUE(host.LayersThatShouldPushProperties().count(b_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(c_layer));
// After a frame, no layers should need to push properties again.
Compositor().BeginFrame();
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(a_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(b_layer));
EXPECT_FALSE(host.LayersThatShouldPushProperties().count(c_layer));
}
TEST_P(CompositingSimTest,
NoopChangeDoesNotCauseFullTreeSyncOrPropertyTreeUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
div {
width: 100px;
height: 100px;
will-change: transform;
}
</style>
<div></div>
)HTML");
Compositor().BeginFrame();
// Initially the host should not need to sync.
cc::LayerTreeHost& layer_tree_host = *Compositor().LayerTreeHost();
EXPECT_FALSE(layer_tree_host.needs_full_tree_sync());
int sequence_number = GetPropertyTrees()->sequence_number;
EXPECT_GT(sequence_number, 0);
// A no-op update should not cause the host to need a full tree sync.
UpdateAllLifecyclePhases();
EXPECT_FALSE(layer_tree_host.needs_full_tree_sync());
// It should also not cause a property tree update - the sequence number
// should not change.
EXPECT_EQ(sequence_number, GetPropertyTrees()->sequence_number);
}
// When a property tree change occurs that affects layer transform in the
// general case, all layers associated with the changed property tree node, and
// all layers associated with a descendant of the changed property tree node
// need to have |subtree_property_changed| set for damage tracking. In
// non-layer-list mode, this occurs in BuildPropertyTreesInternal (see:
// SetLayerPropertyChangedForChild).
TEST_P(CompositingSimTest, LayerSubtreeTransformPropertyChanged) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
transform: translate(10px, 10px);
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto* inner_element_layer = CcLayerByDOMElementId("inner");
// Initially, no layer should have |subtree_property_changed| set.
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(GetTransformNode(outer_element_layer)->transform_changed);
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetTransformNode(inner_element_layer)->transform_changed);
// Modifying the transform style should set |subtree_property_changed| on
// both layers.
outer_element->setAttribute(html_names::kStyleAttr,
"transform: rotate(10deg)");
UpdateAllLifecyclePhases();
// This is still set by the traditional GraphicsLayer::SetTransform().
EXPECT_TRUE(outer_element_layer->subtree_property_changed());
// Set by blink::PropertyTreeManager.
EXPECT_TRUE(GetTransformNode(outer_element_layer)->transform_changed);
// TODO(wangxianzhu): Probably avoid setting this flag on transform change.
EXPECT_TRUE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetTransformNode(inner_element_layer)->transform_changed);
// After a frame the |subtree_property_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(GetTransformNode(outer_element_layer)->transform_changed);
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetTransformNode(inner_element_layer)->transform_changed);
}
// When a property tree change occurs that affects layer transform in a simple
// case (ie before and after transforms both preserve axis alignment), the
// transforms can be directly updated without explicitly marking layers as
// damaged. The ensure damage occurs, the transform node should have
// |transform_changed| set. In non-layer-list mode, this occurs in
// cc::TransformTree::OnTransformAnimated and cc::Layer::SetTransform.
TEST_P(CompositingSimTest, DirectTransformPropertyUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
transform: translate(10px, 10px) scale(1, 2);
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto transform_tree_index = outer_element_layer->transform_tree_index();
auto* transform_node =
GetPropertyTrees()->transform_tree.Node(transform_tree_index);
// Initially, transform should be unchanged.
EXPECT_FALSE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Modifying the transform in a simple way allowed for a direct update.
outer_element->setAttribute(html_names::kStyleAttr,
"transform: translate(30px, 20px) scale(5, 5)");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// After a frame the |transform_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(transform_node->transform_changed);
}
TEST_P(CompositingSimTest, DirectSVGTransformPropertyUpdate) {
InitializeWithHTML(R"HTML(
<!doctype html>
<style>
#willChange {
width: 100px;
height: 100px;
will-change: transform;
transform: translate(10px, 10px);
}
</style>
<svg width="200" height="200">
<rect id="willChange" fill="blue"></rect>
</svg>
)HTML");
Compositor().BeginFrame();
auto* will_change_layer = CcLayerByDOMElementId("willChange");
auto transform_tree_index = will_change_layer->transform_tree_index();
auto* transform_node =
GetPropertyTrees()->transform_tree.Node(transform_tree_index);
// Initially, transform should be unchanged.
EXPECT_FALSE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Modifying the transform in a simple way allowed for a direct update.
auto* will_change_element = GetElementById("willChange");
will_change_element->setAttribute(html_names::kStyleAttr,
"transform: translate(30px, 20px)");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// After a frame the |transform_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(transform_node->transform_changed);
}
// This test is similar to |DirectTransformPropertyUpdate| but tests that
// the changed value of a directly updated transform is still set if some other
// change causes PaintArtifactCompositor to run and do non-direct updates.
TEST_P(CompositingSimTest, DirectTransformPropertyUpdateCausesChange) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
transform: translate(1px, 2px);
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
transform: translate(3px, 4px);
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto outer_transform_tree_index = outer_element_layer->transform_tree_index();
auto* outer_transform_node =
GetPropertyTrees()->transform_tree.Node(outer_transform_tree_index);
auto* inner_element = GetElementById("inner");
auto* inner_element_layer = CcLayerByDOMElementId("inner");
auto inner_transform_tree_index = inner_element_layer->transform_tree_index();
auto* inner_transform_node =
GetPropertyTrees()->transform_tree.Node(inner_transform_tree_index);
// Initially, the transforms should be unchanged.
EXPECT_FALSE(outer_transform_node->transform_changed);
EXPECT_FALSE(inner_transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Modifying the outer transform in a simple way should allow for a direct
// update of the outer transform. Modifying the inner transform in a
// non-simple way should not allow for a direct update of the inner transform.
outer_element->setAttribute(html_names::kStyleAttr,
"transform: translate(5px, 6px)");
inner_element->setAttribute(html_names::kStyleAttr,
"transform: rotate(30deg)");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(outer_transform_node->transform_changed);
EXPECT_FALSE(inner_transform_node->transform_changed);
EXPECT_TRUE(paint_artifact_compositor()->NeedsUpdate());
// After a PaintArtifactCompositor update, which was needed due to the inner
// element's transform change, both the inner and outer transform nodes
// should be marked as changed to ensure they result in damage.
UpdateAllLifecyclePhases();
EXPECT_TRUE(outer_transform_node->transform_changed);
EXPECT_TRUE(inner_transform_node->transform_changed);
// After a frame the |transform_changed| values should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(outer_transform_node->transform_changed);
EXPECT_FALSE(inner_transform_node->transform_changed);
}
// This test ensures that the correct transform nodes are created and bits set
// so that the browser controls movement adjustments needed by bottom-fixed
// elements will work.
TEST_P(CompositingSimTest, AffectedByOuterViewportBoundsDelta) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
body { height: 2000px; }
#fixed {
width: 100px;
height: 100px;
position: fixed;
left: 0;
background-color: red;
}
</style>
<div id='fixed'></div>
)HTML");
auto* fixed_element = GetElementById("fixed");
auto* fixed_element_layer = CcLayerByDOMElementId("fixed");
// Fix the DIV to the bottom of the viewport. Since the viewport height will
// expand/contract, the fixed element will need to be moved as the bounds
// delta changes.
{
fixed_element->setAttribute(html_names::kStyleAttr, "bottom: 0");
Compositor().BeginFrame();
auto transform_tree_index = fixed_element_layer->transform_tree_index();
auto* transform_node =
GetPropertyTrees()->transform_tree.Node(transform_tree_index);
DCHECK(transform_node);
EXPECT_TRUE(transform_node->moved_by_outer_viewport_bounds_delta_y);
}
// Fix it to the top now. Since the top edge doesn't change (relative to the
// renderer origin), we no longer need to move it as the bounds delta
// changes.
{
fixed_element->setAttribute(html_names::kStyleAttr, "top: 0");
Compositor().BeginFrame();
auto transform_tree_index = fixed_element_layer->transform_tree_index();
auto* transform_node =
GetPropertyTrees()->transform_tree.Node(transform_tree_index);
DCHECK(transform_node);
EXPECT_FALSE(transform_node->moved_by_outer_viewport_bounds_delta_y);
}
}
// When a property tree change occurs that affects layer transform-origin, the
// transform can be directly updated without explicitly marking the layer as
// damaged. The ensure damage occurs, the transform node should have
// |transform_changed| set. In non-layer-list mode, this occurs in
// cc::Layer::SetTransformOrigin.
TEST_P(CompositingSimTest, DirectTransformOriginPropertyUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#box {
width: 100px;
height: 100px;
transform: rotate3d(3, 2, 1, 45deg);
transform-origin: 10px 10px 100px;
background: lightblue;
}
</style>
<div id='box'></div>
)HTML");
Compositor().BeginFrame();
auto* box_element = GetElementById("box");
auto* box_element_layer = CcLayerByDOMElementId("box");
auto transform_tree_index = box_element_layer->transform_tree_index();
auto* transform_node =
GetPropertyTrees()->transform_tree.Node(transform_tree_index);
// Initially, transform should be unchanged.
EXPECT_FALSE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Modifying the transform-origin in a simple way allowed for a direct update.
box_element->setAttribute(html_names::kStyleAttr,
"transform-origin: -10px -10px -100px");
UpdateAllLifecyclePhasesExceptPaint();
EXPECT_TRUE(transform_node->transform_changed);
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// After a frame the |transform_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(transform_node->transform_changed);
}
// This test is similar to |LayerSubtreeTransformPropertyChanged| but for
// effect property node changes.
TEST_P(CompositingSimTest, LayerSubtreeEffectPropertyChanged) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
filter: blur(10px);
}
#inner {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto* inner_element_layer = CcLayerByDOMElementId("inner");
// Initially, no layer should have |subtree_property_changed| set.
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(GetEffectNode(outer_element_layer)->effect_changed);
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetEffectNode(inner_element_layer)->effect_changed);
// Modifying the filter style should set |subtree_property_changed| on
// both layers.
outer_element->setAttribute(html_names::kStyleAttr, "filter: blur(20px)");
UpdateAllLifecyclePhases();
EXPECT_TRUE(outer_element_layer->subtree_property_changed());
// Set by blink::PropertyTreeManager.
EXPECT_TRUE(GetEffectNode(outer_element_layer)->effect_changed);
EXPECT_TRUE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetEffectNode(inner_element_layer)->effect_changed);
// After a frame the |subtree_property_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(GetEffectNode(outer_element_layer)->effect_changed);
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
EXPECT_FALSE(GetEffectNode(inner_element_layer)->effect_changed);
}
// This test is similar to |LayerSubtreeTransformPropertyChanged| but for
// clip property node changes.
TEST_P(CompositingSimTest, LayerSubtreeClipPropertyChanged) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
position: absolute;
clip: rect(10px, 80px, 70px, 40px);
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto* inner_element_layer = CcLayerByDOMElementId("inner");
// Initially, no layer should have |subtree_property_changed| set.
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
// Modifying the clip style should set |subtree_property_changed| on
// both layers.
outer_element->setAttribute(html_names::kStyleAttr,
"clip: rect(1px, 8px, 7px, 4px);");
UpdateAllLifecyclePhases();
EXPECT_TRUE(outer_element_layer->subtree_property_changed());
EXPECT_TRUE(inner_element_layer->subtree_property_changed());
// After a frame the |subtree_property_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
}
TEST_P(CompositingSimTest, LayerSubtreeOverflowClipPropertyChanged) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
html { overflow: hidden; }
#outer {
width: 100px;
height: 100px;
will-change: transform;
position: absolute;
overflow: hidden;
}
#inner {
width: 200px;
height: 100px;
will-change: transform;
background: lightblue;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* outer_element = GetElementById("outer");
auto* outer_element_layer = CcLayerByDOMElementId("outer");
auto* inner_element_layer = CcLayerByDOMElementId("inner");
// Initially, no layer should have |subtree_property_changed| set.
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
// Modifying the clip width should set |subtree_property_changed| on
// both layers.
outer_element->setAttribute(html_names::kStyleAttr, "width: 200px;");
UpdateAllLifecyclePhases();
// The overflow clip does not affect |outer_element_layer|, so
// subtree_property_changed should be false for it. It does affect
// |inner_element_layer| though.
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_TRUE(inner_element_layer->subtree_property_changed());
// After a frame the |subtree_property_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(outer_element_layer->subtree_property_changed());
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
}
// This test is similar to |LayerSubtreeClipPropertyChanged| but for cases when
// the clip node itself does not change but the clip node associated with a
// layer changes.
TEST_P(CompositingSimTest, LayerClipPropertyChanged) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#outer {
width: 100px;
height: 100px;
}
#inner {
width: 50px;
height: 200px;
backface-visibility: hidden;
background: lightblue;
}
</style>
<div id='outer' style='overflow: hidden;'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* inner_element_layer = CcLayerByDOMElementId("inner");
EXPECT_TRUE(inner_element_layer->should_check_backface_visibility());
// Initially, no layer should have |subtree_property_changed| set.
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
// Removing overflow: hidden on the outer div should set
// |subtree_property_changed| on the inner div's cc::Layer.
auto* outer_element = GetElementById("outer");
outer_element->setAttribute(html_names::kStyleAttr, "");
UpdateAllLifecyclePhases();
inner_element_layer = CcLayerByDOMElementId("inner");
EXPECT_TRUE(inner_element_layer->should_check_backface_visibility());
EXPECT_TRUE(inner_element_layer->subtree_property_changed());
// After a frame the |subtree_property_changed| value should be reset.
Compositor().BeginFrame();
EXPECT_FALSE(inner_element_layer->subtree_property_changed());
}
TEST_P(CompositingSimTest, SafeOpaqueBackgroundColor) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
body { background: yellow; }
div {
position: absolute;
z-index: 1;
width: 20px;
height: 20px;
will-change: transform; /* Composited */
}
#opaque-color {
background: blue;
}
#opaque-image, #opaque-image-translucent-color {
background: linear-gradient(blue, green);
}
#partly-opaque div {
width: 15px;
height: 15px;
background: blue;
will-change: initial;
}
#translucent, #opaque-image-translucent-color div {
background: rgba(0, 255, 255, 0.5);
will-change: initial;
}
</style>
<div id="opaque-color"></div>
<div id="opaque-image"></div>
<div id="opaque-image-translucent-color">
<div></div>
</div>
<div id="partly-opaque">
<div></div>
</div>
<div id="translucent"></div>
)HTML");
Compositor().BeginFrame();
auto* opaque_color = CcLayerByDOMElementId("opaque-color");
EXPECT_TRUE(opaque_color->contents_opaque());
EXPECT_EQ(SK_ColorBLUE, opaque_color->background_color());
EXPECT_EQ(SK_ColorBLUE, opaque_color->SafeOpaqueBackgroundColor());
auto* opaque_image = CcLayerByDOMElementId("opaque-image");
EXPECT_TRUE(opaque_image->contents_opaque());
EXPECT_EQ(SK_ColorTRANSPARENT, opaque_image->background_color());
// Fallback to use the viewport background.
EXPECT_EQ(SK_ColorYELLOW, opaque_image->SafeOpaqueBackgroundColor());
const SkColor kTranslucentCyan = SkColorSetARGB(128, 0, 255, 255);
auto* opaque_image_translucent_color =
CcLayerByDOMElementId("opaque-image-translucent-color");
EXPECT_TRUE(opaque_image_translucent_color->contents_opaque());
EXPECT_EQ(kTranslucentCyan,
opaque_image_translucent_color->background_color());
// Use background_color() with the alpha channel forced to be opaque.
EXPECT_EQ(SK_ColorCYAN,
opaque_image_translucent_color->SafeOpaqueBackgroundColor());
auto* partly_opaque = CcLayerByDOMElementId("partly-opaque");
EXPECT_FALSE(partly_opaque->contents_opaque());
EXPECT_EQ(SK_ColorBLUE, partly_opaque->background_color());
// SafeOpaqueBackgroundColor() returns SK_ColorTRANSPARENT when
// background_color() is opaque and contents_opaque() is false.
EXPECT_EQ(SK_ColorTRANSPARENT, partly_opaque->SafeOpaqueBackgroundColor());
auto* translucent = CcLayerByDOMElementId("translucent");
EXPECT_FALSE(translucent->contents_opaque());
EXPECT_EQ(kTranslucentCyan, translucent->background_color());
// SafeOpaqueBackgroundColor() returns background_color() if it's not opaque
// and contents_opaque() is false.
EXPECT_EQ(kTranslucentCyan, translucent->SafeOpaqueBackgroundColor());
}
TEST_P(CompositingSimTest, SquashingLayerSafeOpaqueBackgroundColor) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
div {
position: absolute;
z-index: 1;
width: 20px;
height: 20px;
}
#behind {
top: 12px;
left: 12px;
background: blue;
will-change: transform; /* Composited */
}
#topleft {
top: 0px;
left: 0px;
background: lime;
}
#bottomright {
top: 24px;
left: 24px;
width: 100px;
height: 100px;
background: cyan;
}
</style>
<div id="behind"></div>
<div id="topleft"></div>
<div id="bottomright"></div>
)HTML");
Compositor().BeginFrame();
auto* squashing_layer = CcLayerByDOMElementId("topleft");
ASSERT_TRUE(squashing_layer);
EXPECT_EQ(gfx::Size(124, 124), squashing_layer->bounds());
// Top left and bottom right are squashed.
// This squashed layer should not be opaque, as it is squashing two squares
// with some gaps between them.
EXPECT_FALSE(squashing_layer->contents_opaque());
// The background color of #bottomright is used as the background color
// because it covers the most significant area of the squashing layer.
EXPECT_EQ(squashing_layer->background_color(), SK_ColorCYAN);
// SafeOpaqueBackgroundColor() returns SK_ColorTRANSPARENT when
// background_color() is opaque and contents_opaque() is false.
EXPECT_EQ(squashing_layer->SafeOpaqueBackgroundColor(), SK_ColorTRANSPARENT);
}
// Test that a pleasant checkerboard color is used in the presence of blending.
TEST_P(CompositingSimTest, RootScrollingContentsSafeOpaqueBackgroundColor) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<div style="mix-blend-mode: multiply;"></div>
<div id="forcescroll" style="height: 10000px;"></div>
)HTML");
Compositor().BeginFrame();
auto* scrolling_contents = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(),
MainFrame().GetFrameView()->LayoutViewport()->GetScrollElementId());
EXPECT_EQ(scrolling_contents->background_color(), SK_ColorWHITE);
EXPECT_EQ(scrolling_contents->SafeOpaqueBackgroundColor(), SK_ColorWHITE);
}
TEST_P(CompositingSimTest, NonDrawableLayersIgnoredForRenderSurfaces) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#outer {
width: 100px;
height: 100px;
opacity: 0.5;
background: blue;
}
#inner {
width: 10px;
height: 10px;
will-change: transform;
}
</style>
<div id='outer'>
<div id='inner'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* inner_element_layer = CcLayerByDOMElementId("inner");
EXPECT_FALSE(inner_element_layer->DrawsContent());
auto* outer_element_layer = CcLayerByDOMElementId("outer");
EXPECT_TRUE(outer_element_layer->DrawsContent());
// The inner element layer is only needed for hit testing and does not draw
// content, so it should not cause a render surface.
auto effect_tree_index = outer_element_layer->effect_tree_index();
auto* effect_node = GetPropertyTrees()->effect_tree.Node(effect_tree_index);
EXPECT_EQ(effect_node->opacity, 0.5f);
EXPECT_FALSE(effect_node->HasRenderSurface());
}
TEST_P(CompositingSimTest, NoRenderSurfaceWithAxisAlignedTransformAnimation) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
@keyframes translation {
0% { transform: translate(10px, 11px); }
100% { transform: translate(20px, 21px); }
}
.animate {
animation-name: translation;
animation-duration: 1s;
width: 100px;
height: 100px;
overflow: hidden;
}
.compchild {
height: 200px;
width: 10px;
background: lightblue;
will-change: transform;
}
</style>
<div class="animate"><div class="compchild"></div></div>
)HTML");
Compositor().BeginFrame();
// No effect node with kClipAxisAlignment should be created because the
// animation is axis-aligned.
for (const auto& effect_node : GetPropertyTrees()->effect_tree.nodes()) {
EXPECT_NE(cc::RenderSurfaceReason::kClipAxisAlignment,
effect_node.render_surface_reason);
}
}
TEST_P(CompositingSimTest, PromoteCrossOriginIframe) {
InitializeWithHTML("<!DOCTYPE html><iframe id=iframe sandbox></iframe>");
Compositor().BeginFrame();
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
auto* layer = CcLayerByOwnerNodeId(owner_node);
EXPECT_TRUE(layer);
EXPECT_EQ(layer->bounds(), gfx::Size(300, 150));
}
// On initial layout, the iframe is not yet loaded and is not considered
// cross origin. This test ensures the iframe is promoted due to being cross
// origin after the iframe loads.
TEST_P(CompositingSimTest, PromoteCrossOriginIframeAfterLoading) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
SimRequest frame_resource("https://origin-b.com/b.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="iframe" src="https://origin-b.com/b.html"></iframe>
)HTML");
frame_resource.Complete("<!DOCTYPE html>");
Compositor().BeginFrame();
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_TRUE(CcLayerByOwnerNodeId(owner_node));
}
// An iframe that is cross-origin to the parent should be composited. This test
// sets up nested frames with domains A -> B -> A. Both the child and grandchild
// frames should be composited because they are cross-origin to their parent.
TEST_P(CompositingSimTest, PromoteCrossOriginToParent) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
SimRequest child_resource("https://origin-b.com/b.html", "text/html");
SimRequest grandchild_resource("https://origin-a.com/c.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="main_iframe" src="https://origin-b.com/b.html"></iframe>
)HTML");
child_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="child_iframe" src="https://origin-a.com/c.html"></iframe>
)HTML");
grandchild_resource.Complete("<!DOCTYPE html>");
Compositor().BeginFrame();
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("main_iframe"))
->contentDocument();
EXPECT_TRUE(CcLayerByOwnerNodeId(iframe_doc));
iframe_doc =
To<HTMLFrameOwnerElement>(iframe_doc->getElementById("child_iframe"))
->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_TRUE(CcLayerByOwnerNodeId(owner_node));
}
// Initially the iframe is cross-origin and should be composited. After changing
// to same-origin, the frame should no longer be composited.
TEST_P(CompositingSimTest, PromoteCrossOriginIframeAfterDomainChange) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
SimRequest frame_resource("https://sub.origin-a.com/b.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="iframe" src="https://sub.origin-a.com/b.html"></iframe>
)HTML");
frame_resource.Complete("<!DOCTYPE html>");
Compositor().BeginFrame();
auto* iframe_element =
To<HTMLIFrameElement>(GetDocument().getElementById("iframe"));
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_TRUE(CcLayerByOwnerNodeId(owner_node));
NonThrowableExceptionState exception_state;
GetDocument().setDomain(String("origin-a.com"), exception_state);
iframe_element->contentDocument()->setDomain(String("origin-a.com"),
exception_state);
// We may not have scheduled a visual update so force an update instead of
// using BeginFrame.
UpdateAllLifecyclePhases();
iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_FALSE(CcLayerByOwnerNodeId(owner_node));
}
// This test sets up nested frames with domains A -> B -> A. Initially, the
// child frame and grandchild frame should be composited. After changing the
// child frame to A (same-origin), both child and grandchild frames should no
// longer be composited.
TEST_P(CompositingSimTest, PromoteCrossOriginToParentIframeAfterDomainChange) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
SimRequest child_resource("https://sub.origin-a.com/b.html", "text/html");
SimRequest grandchild_resource("https://origin-a.com/c.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="main_iframe" src="https://sub.origin-a.com/b.html"></iframe>
)HTML");
child_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="child_iframe" src="https://origin-a.com/c.html"></iframe>
)HTML");
grandchild_resource.Complete("<!DOCTYPE html>");
Compositor().BeginFrame();
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("main_iframe"))
->contentDocument();
EXPECT_TRUE(CcLayerByOwnerNodeId(iframe_doc));
iframe_doc =
To<HTMLFrameOwnerElement>(iframe_doc->getElementById("child_iframe"))
->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_TRUE(CcLayerByOwnerNodeId(owner_node));
auto* main_iframe_element =
To<HTMLIFrameElement>(GetDocument().getElementById("main_iframe"));
NonThrowableExceptionState exception_state;
GetDocument().setDomain(String("origin-a.com"), exception_state);
auto* child_iframe_element = To<HTMLIFrameElement>(
main_iframe_element->contentDocument()->getElementById("child_iframe"));
child_iframe_element->contentDocument()->setDomain(String("origin-a.com"),
exception_state);
main_iframe_element->contentDocument()->setDomain(String("origin-a.com"),
exception_state);
// We may not have scheduled a visual update so force an update instead of
// using BeginFrame.
UpdateAllLifecyclePhases();
iframe_doc = To<HTMLFrameOwnerElement>(GetElementById("main_iframe"))
->contentDocument();
EXPECT_FALSE(CcLayerByOwnerNodeId(iframe_doc));
iframe_doc =
To<HTMLFrameOwnerElement>(iframe_doc->getElementById("child_iframe"))
->contentDocument();
owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
EXPECT_FALSE(CcLayerByOwnerNodeId(owner_node));
}
// Regression test for https://crbug.com/1095167. Render surfaces require that
// EffectNode::stable_id is set.
TEST_P(CompositingTest, EffectNodesShouldHaveStableIds) {
InitializeWithHTML(*WebView()->MainFrameImpl()->GetFrame(), R"HTML(
<div style="overflow: hidden; border-radius: 2px; height: 10px;">
<div style="backdrop-filter: grayscale(3%);">
a
<span style="backdrop-filter: grayscale(3%);">b</span>
</div>
</div>
)HTML");
auto* property_trees = RootCcLayer()->layer_tree_host()->property_trees();
for (const auto& effect_node : property_trees->effect_tree.nodes()) {
if (effect_node.parent_id != -1)
EXPECT_TRUE(!!effect_node.stable_id);
}
}
TEST_P(CompositingSimTest, ImplSideScrollSkipsCommit) {
InitializeWithHTML(R"HTML(
<div id='scroller' style='will-change: transform; overflow: scroll;
width: 100px; height: 100px'>
<div style='height: 1000px'></div>
</div>
)HTML");
Compositor().BeginFrame();
auto* scroller = GetDocument().getElementById("scroller");
auto* scrollable_area = scroller->GetLayoutBox()->GetScrollableArea();
auto element_id = scrollable_area->GetScrollElementId();
EXPECT_FALSE(Compositor().LayerTreeHost()->CommitRequested());
// Simulate the scroll update with scroll delta from impl-side.
cc::CompositorCommitData commit_data;
commit_data.scrolls.emplace_back(cc::CompositorCommitData::ScrollUpdateInfo(
element_id, gfx::ScrollOffset(0, 10), absl::nullopt));
Compositor().LayerTreeHost()->ApplyCompositorChanges(&commit_data);
EXPECT_EQ(FloatPoint(0, 10), scrollable_area->ScrollPosition());
EXPECT_EQ(gfx::ScrollOffset(0, 10),
GetPropertyTrees()->scroll_tree.current_scroll_offset(element_id));
// Update just the blink lifecycle because a full frame would clear the bit
// for whether a commit was requested.
UpdateAllLifecyclePhases();
// A main frame is needed to call UpdateLayers which updates property trees,
// re-calculating cached to/from-screen transforms.
EXPECT_TRUE(
Compositor().LayerTreeHost()->RequestedMainFramePendingForTesting());
// A full commit is not needed.
EXPECT_FALSE(Compositor().LayerTreeHost()->CommitRequested());
}
TEST_P(CompositingSimTest, FrameAttribution) {
InitializeWithHTML(R"HTML(
<div id='child' style='will-change: transform;'>test</div>
<iframe id='iframe' sandbox></iframe>
)HTML");
Compositor().BeginFrame();
// Ensure that we correctly attribute child layers in the main frame to their
// containing document.
auto* child_layer = CcLayerByDOMElementId("child");
ASSERT_TRUE(child_layer);
auto* child_transform_node = GetTransformNode(child_layer);
ASSERT_TRUE(child_transform_node);
// Iterate the transform tree to gather the parent frame element ID.
cc::ElementId visible_frame_element_id;
auto* current_transform_node = child_transform_node;
while (current_transform_node) {
visible_frame_element_id = current_transform_node->visible_frame_element_id;
if (visible_frame_element_id)
break;
current_transform_node =
GetPropertyTrees()->transform_tree.parent(current_transform_node);
}
EXPECT_EQ(visible_frame_element_id,
CompositorElementIdFromUniqueObjectId(
DOMNodeIds::IdForNode(&GetDocument()),
CompositorElementIdNamespace::kDOMNodeId));
// Test that a layerized subframe's frame element ID is that of its
// containing document.
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
auto* iframe_layer = CcLayerByOwnerNodeId(owner_node);
ASSERT_TRUE(iframe_layer);
auto* iframe_transform_node = GetTransformNode(iframe_layer);
EXPECT_TRUE(iframe_transform_node);
EXPECT_EQ(iframe_transform_node->visible_frame_element_id,
CompositorElementIdFromUniqueObjectId(
DOMNodeIds::IdForNode(iframe_doc),
CompositorElementIdNamespace::kDOMNodeId));
}
TEST_P(CompositingSimTest, VisibleFrameRootLayers) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
SimRequest frame_resource("https://origin-b.com/b.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<iframe id="iframe" src="https://origin-b.com/b.html"></iframe>
)HTML");
frame_resource.Complete("<!DOCTYPE html>");
Compositor().BeginFrame();
// Ensure that the toplevel is marked as a visible root.
auto* toplevel_layer = CcLayerByOwnerNodeId(&GetDocument());
ASSERT_TRUE(toplevel_layer);
auto* toplevel_transform_node = GetTransformNode(toplevel_layer);
ASSERT_TRUE(toplevel_transform_node);
EXPECT_TRUE(toplevel_transform_node->visible_frame_element_id);
// Ensure that the iframe is marked as a visible root.
Document* iframe_doc =
To<HTMLFrameOwnerElement>(GetElementById("iframe"))->contentDocument();
Node* owner_node = iframe_doc;
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
owner_node = iframe_doc->documentElement();
auto* iframe_layer = CcLayerByOwnerNodeId(owner_node);
ASSERT_TRUE(iframe_layer);
auto* iframe_transform_node = GetTransformNode(iframe_layer);
ASSERT_TRUE(iframe_transform_node);
EXPECT_TRUE(iframe_transform_node->visible_frame_element_id);
// Verify that after adding `pointer-events: none`, the subframe is no longer
// considered a visible root.
GetElementById("iframe")->SetInlineStyleProperty(
CSSPropertyID::kPointerEvents, "none");
UpdateAllLifecyclePhases();
iframe_layer = CcLayerByOwnerNodeId(owner_node);
ASSERT_TRUE(iframe_layer);
iframe_transform_node = GetTransformNode(iframe_layer);
ASSERT_TRUE(iframe_transform_node);
EXPECT_FALSE(iframe_transform_node->visible_frame_element_id);
}
TEST_P(CompositingSimTest, DecompositedTransformWithChange) {
InitializeWithHTML(R"HTML(
<style>
svg { overflow: hidden; }
.initial { transform: rotate3d(0,0,1,10deg); }
.changed { transform: rotate3d(0,0,1,0deg); }
</style>
<div style='will-change: transform;'>
<svg id='svg' xmlns='http://www.w3.org/2000/svg' class='initial'>
<line x1='50%' x2='50%' y1='0' y2='100%' stroke='blue'/>
<line y1='50%' y2='50%' x1='0' x2='100%' stroke='blue'/>
</svg>
</div>
)HTML");
Compositor().BeginFrame();
auto* svg_element_layer = CcLayerByDOMElementId("svg");
EXPECT_FALSE(svg_element_layer->subtree_property_changed());
auto* svg_element = GetElementById("svg");
svg_element->setAttribute(html_names::kClassAttr, "changed");
UpdateAllLifecyclePhases();
EXPECT_TRUE(svg_element_layer->subtree_property_changed());
}
// A simple repaint update should use a fast-path in PaintArtifactCompositor.
TEST_P(CompositingSimTest, BackgroundColorChangeUsesRepaintUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#target {
width: 100px;
height: 100px;
will-change: transform;
background: white;
}
</style>
<div id='target'></div>
)HTML");
Compositor().BeginFrame();
EXPECT_EQ(CcLayerByDOMElementId("target")->background_color(), SK_ColorWHITE);
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Modifying paint in a simple way only requires a repaint update.
auto* target_element = GetElementById("target");
target_element->setAttribute(html_names::kStyleAttr, "background: black");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kRepaint);
// Though a repaint-only update was done, the background color should still
// be updated.
EXPECT_EQ(CcLayerByDOMElementId("target")->background_color(), SK_ColorBLACK);
}
// Similar to |BackgroundColorChangeUsesRepaintUpdate| but with multiple paint
// chunks being squashed into a single PendingLayer, and the background coming
// from the last paint chunk.
TEST_P(CompositingSimTest, MultipleChunkBackgroundColorChangeRepaintUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
div {
position: absolute;
width: 20px;
height: 20px;
top: 0px;
left: 0px;
}
#a {
background: lime;
}
#b {
background: red;
transform: translate(-100px, -100px);
}
#c {
width: 800px;
height: 600px;
background: black;
}
</style>
<div id="a"></div>
<div id="b"></div>
<!-- background color source -->
<div id="c"></div>
)HTML");
Compositor().BeginFrame();
auto* scrolling_contents = ScrollingContentsCcLayerByScrollElementId(
RootCcLayer(),
MainFrame().GetFrameView()->LayoutViewport()->GetScrollElementId());
EXPECT_EQ(scrolling_contents->background_color(), SK_ColorBLACK);
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Modifying paint in a simple way only requires a repaint update.
auto* background_element = GetElementById("c");
background_element->setAttribute(html_names::kStyleAttr, "background: white");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kRepaint);
// Though a repaint-only update was done, the background color should still
// be updated.
EXPECT_EQ(scrolling_contents->background_color(), SK_ColorWHITE);
}
// Similar to |BackgroundColorChangeUsesRepaintUpdate| but with post-paint
// composited SVG. This test changes paint for a composited SVG element, as well
// as a regular HTML element in the presence of composited SVG.
TEST_P(CompositingSimTest, SVGColorChangeUsesRepaintUpdate) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
rect, div {
width: 100px;
height: 100px;
will-change: transform;
}
</style>
<svg>
<rect fill="blue" />
<rect id="rect" fill="blue" />
<rect fill="blue" />
</svg>
<div id="div" style="background: blue;" />
<svg>
<rect fill="blue" />
</svg>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Modifying paint in a simple way only requires a repaint update.
auto* rect_element = GetElementById("rect");
rect_element->setAttribute(svg_names::kFillAttr, "black");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kRepaint);
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Modifying paint in a simple way only requires a repaint update.
auto* div_element = GetElementById("div");
div_element->setAttribute(html_names::kStyleAttr, "background: black");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kRepaint);
}
TEST_P(CompositingSimTest, ChangingOpaquenessRequiresFullUpdate) {
// Contents opaque is set in different places in CAP (PAC) vs pre-CAP (CLM)
// and we only want to test the PAC update here.
if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return;
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#target {
width: 100px;
height: 100px;
will-change: transform;
background: lightgreen;
}
</style>
<div id="target"></div>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
EXPECT_TRUE(CcLayerByDOMElementId("target")->contents_opaque());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// A change in opaqueness still requires a full update because opaqueness is
// used during compositing to set the cc::Layer's contents opaque property
// (see: PaintArtifactCompositor::CompositedLayerForPendingLayer).
auto* target_element = GetElementById("target");
target_element->setAttribute(html_names::kStyleAttr,
"background: rgba(1, 0, 0, 0.1)");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
EXPECT_FALSE(CcLayerByDOMElementId("target")->contents_opaque());
}
TEST_P(CompositingSimTest, ChangingContentsOpaqueForTextRequiresFullUpdate) {
// Contents opaque for text is set in different places in CAP (PAC) vs pre-CAP
// (CLM) and we only want to test the PAC update here.
if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return;
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#target {
width: 100px;
height: 100px;
will-change: transform;
}
#textContainer {
width: 50px;
height: 50px;
padding: 5px;
background: lightblue;
}
}
</style>
<div id="target">
<div id="textContainer">
mars
</div>
</div>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
EXPECT_FALSE(CcLayerByDOMElementId("target")->contents_opaque());
EXPECT_TRUE(CcLayerByDOMElementId("target")->contents_opaque_for_text());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// A change in opaqueness for text still requires a full update because
// opaqueness is used during compositing to set the cc::Layer's contents
// opaque for text property (see:
// PaintArtifactCompositor::CompositedLayerForPendingLayer).
auto* text_container_element = GetElementById("textContainer");
text_container_element->setAttribute(html_names::kStyleAttr,
"background: rgba(1, 0, 0, 0.1)");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
EXPECT_FALSE(CcLayerByDOMElementId("target")->contents_opaque());
EXPECT_FALSE(CcLayerByDOMElementId("target")->contents_opaque_for_text());
}
TEST_P(CompositingSimTest, FullCompositingUpdateReasons) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
div {
width: 100px;
height: 100px;
will-change: transform;
position: absolute;
}
#a {
background: lightgreen;
z-index: 10;
}
#b {
background: lightblue;
z-index: 20;
}
</style>
<div id="a"></div>
<div id="b"></div>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Reordering paint chunks requires a full update. Overlap testing and the
// order of synthetic effect layers are two examples of paint changes that
// affect compositing decisions.
auto* b_element = GetElementById("b");
b_element->setAttribute(html_names::kStyleAttr, "z-index: 5");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Removing a paint chunk requires a full update.
b_element->setAttribute(html_names::kStyleAttr, "display: none");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Adding a paint chunk requires a full update.
b_element->setAttribute(html_names::kStyleAttr, "");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Changing the size of a chunk affects overlap and requires a full update.
b_element->setAttribute(html_names::kStyleAttr, "width: 101px");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
}
// Similar to |FullCompositingUpdateReasons| but for changes in post-paint
// composited SVG.
TEST_P(CompositingSimTest, FullCompositingUpdateReasonWithCompositedSVG) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#rect {
width: 100px;
height: 100px;
will-change: transform;
}
</style>
<svg>
<rect id="rect" fill="blue" />
</svg>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Changing the size of a chunk affects overlap and requires a full update.
auto* rect = GetElementById("rect");
rect->setAttribute(html_names::kStyleAttr, "width: 101px");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
}
TEST_P(CompositingSimTest, FullCompositingUpdateForJustCreatedChunks) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
.firstLetterStyle:first-letter {
background: red;
}
rect {
width: 100px;
height: 100px;
fill: blue;
}
</style>
<svg>
<rect style="will-change: transform;"></rect>
<rect id="target"></rect>
</svg>
)HTML");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// A new LayoutObject is "just created" and will not match existing chunks and
// needs a full update. A first letter style adds a pseudo element which
// results in rebuilding the #target LayoutObject.
auto* target = GetElementById("target");
target->setAttribute(html_names::kClassAttr, "firstLetterStyle");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
}
TEST_P(CompositingSimTest, FullCompositingUpdateForUncachableChunks) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
rect {
width: 100px;
height: 100px;
fill: blue;
will-change: transform;
}
div {
width: 100px;
height: 100px;
background: lightblue;
}
</style>
<svg>
<rect id="rect"></rect>
</svg>
<div id="target"></div>
)HTML");
Compositor().BeginFrame();
// Make the rect display item client uncachable. To avoid depending on when
// this occurs in practice (see: |DisplayItemCacheSkipper|), this is done
// directly.
auto* rect = GetElementById("rect");
auto* rect_client = static_cast<DisplayItemClient*>(rect->GetLayoutObject());
rect_client->Invalidate(PaintInvalidationReason::kUncacheable);
rect->setAttribute(html_names::kStyleAttr, "fill: green");
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// A full update should be required due to the presence of uncacheable
// paint chunks.
auto* target = GetElementById("target");
target->setAttribute(html_names::kStyleAttr, "background: lightgreen");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kFull);
}
TEST_P(CompositingSimTest, DecompositeScrollerInHiddenIframe) {
if (RuntimeEnabledFeatures::CompositeAfterPaintEnabled())
return;
SimRequest top_resource("https://example.com/top.html", "text/html");
SimRequest middle_resource("https://example.com/middle.html", "text/html");
SimRequest bottom_resource("https://example.com/bottom.html", "text/html");
LoadURL("https://example.com/top.html");
top_resource.Complete(R"HTML(
<iframe id='middle' src='https://example.com/middle.html'></iframe>
)HTML");
middle_resource.Complete(R"HTML(
<iframe id='bottom' src='bottom.html'></iframe>
)HTML");
bottom_resource.Complete(R"HTML(
<div id='scroller' style='overflow:scroll;max-height:100px;background-color:#888'>
<div style='height:1000px;'>Hello, world!</div>
</div>
)HTML");
LocalFrame& middle_frame =
*To<LocalFrame>(GetDocument().GetFrame()->Tree().FirstChild());
LocalFrame& bottom_frame = *To<LocalFrame>(middle_frame.Tree().FirstChild());
middle_frame.View()->BeginLifecycleUpdates();
bottom_frame.View()->BeginLifecycleUpdates();
Compositor().BeginFrame();
LayoutBox* scroller = To<LayoutBox>(bottom_frame.GetDocument()
->getElementById("scroller")
->GetLayoutObject());
ASSERT_TRUE(scroller->GetScrollableArea()->NeedsCompositedScrolling());
// Hide the iframes. Scroller should be decomposited.
GetDocument().getElementById("middle")->SetInlineStyleProperty(
CSSPropertyID::kVisibility, CSSValueID::kHidden);
Compositor().BeginFrame();
EXPECT_FALSE(scroller->GetScrollableArea()->NeedsCompositedScrolling());
}
TEST_P(CompositingSimTest, ForeignLayersInMovedSubsequence) {
SimRequest main_resource("https://origin-a.com/a.html", "text/html");
LoadURL("https://origin-a.com/a.html");
main_resource.Complete(R"HTML(
<!DOCTYPE html>
<style> iframe { isolation: isolate; } </style>
<iframe sandbox src="https://origin-b.com/b.html"></iframe>
<div id="target" style="background: blue;">a</div>
)HTML");
frame_test_helpers::TestWebRemoteFrameClient remote_frame_client;
FakeRemoteFrameHost remote_frame_host;
remote_frame_host.Init(remote_frame_client.GetRemoteAssociatedInterfaces());
WebRemoteFrameImpl* remote_frame =
frame_test_helpers::CreateRemote(&remote_frame_client);
MainFrame().FirstChild()->Swap(remote_frame);
Compositor().BeginFrame();
auto remote_surface_layer = cc::SurfaceLayer::Create();
remote_frame->GetFrame()->SetCcLayerForTesting(remote_surface_layer, true);
Compositor().BeginFrame();
// Initially, no update is needed.
EXPECT_FALSE(paint_artifact_compositor()->NeedsUpdate());
// Clear the previous update to ensure we record a new one in the next update.
paint_artifact_compositor()->ClearPreviousUpdateForTesting();
// Modifying paint in a simple way only requires a repaint update.
auto* target_element = GetElementById("target");
target_element->setAttribute(html_names::kStyleAttr, "background: green;");
Compositor().BeginFrame();
EXPECT_EQ(paint_artifact_compositor()->PreviousUpdateForTesting(),
PaintArtifactCompositor::PreviousUpdateType::kRepaint);
remote_frame->Detach();
}
// While not required for correctness, it is important for performance that
// snapped backgrounds use solid color layers which avoid tiling.
TEST_P(CompositingSimTest, SolidColorLayersWithSnapping) {
InitializeWithHTML(R"HTML(
<!DOCTYPE html>
<style>
#snapDown {
width: 60.1px;
height: 100px;
will-change: opacity;
background: blue;
}
#snapUp {
width: 60.9px;
height: 100px;
will-change: opacity;
background: blue;
}
</style>
<div id="snapDown"></div>
<div id="snapUp"></div>
)HTML");
Compositor().BeginFrame();
auto* snap_down =
static_cast<const cc::PictureLayer*>(CcLayerByDOMElementId("snapDown"));
EXPECT_TRUE(snap_down->GetRecordingSourceForTesting()->is_solid_color());
auto* snap_up =
static_cast<const cc::PictureLayer*>(CcLayerByDOMElementId("snapUp"));
EXPECT_TRUE(snap_up->GetRecordingSourceForTesting()->is_solid_color());
}
} // namespace blink