0

Full implementation of scale transform render surface trigger

The general approach is as follows:

* Look for 2D scale transforms, and allocate effect nodes for them
(if all goes well, this should have little or no effect on cc layer
count or render surfaces)
* Upgrade the effect node for a 2D scale transform to a render surface
if it doesn't have a backdrop filter in a descendant effect node, and
has at least two composited layers under it.

Also, in particular this approach:

* Relies on implicit compositing of scale transform due to presence of
  composited descendants

Bug: 40084005

Change-Id: If7fff045752f5cf1c09e574a81d181b2a8ac4959
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6299812
Reviewed-by: Xianzhu Wang <wangxianzhu@chromium.org>
Commit-Queue: Chris Harrelson <chrishtr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1427258}
This commit is contained in:
Chris Harrelson
2025-03-03 11:01:08 -08:00
committed by Chromium LUCI CQ
parent 17c501232f
commit eabea0d779
83 changed files with 638 additions and 104 deletions
cc/trees
third_party/blink
renderer
web_tests
css3
external
fast
flag-specific
paint
platform
linux
compositing
fast
transforms
virtual
prefer_compositing_to_lcd_text
mac-mac11-arm64
mac-mac12-arm64
mac-mac13-arm64
mac-mac14-arm64
virtual
gpu-rasterization
mac-mac15-arm64
mac
win
compositing
fast
scrollbars
transforms
virtual
gpu-rasterization
prefer_compositing_to_lcd_text
win10
virtual
prefer_compositing_to_lcd_text
win11-arm64
svg
transforms
wpt_internal

@ -68,8 +68,8 @@ const char* RenderSurfaceReasonToString(RenderSurfaceReason reason) {
return "subtree being captured";
case RenderSurfaceReason::kViewTransitionParticipant:
return "view transition participant";
case RenderSurfaceReason::k2DTransformWithCompositedDescendants:
return "2D transform with composited descendants";
case RenderSurfaceReason::k2DScaleTransformWithCompositedDescendants:
return "2D scale transform with composited descendants";
case RenderSurfaceReason::kTest:
return "test";
}

@ -53,7 +53,7 @@ enum class RenderSurfaceReason : uint8_t {
kSubtreeIsBeingCaptured,
kViewTransitionParticipant,
kGradientMask,
k2DTransformWithCompositedDescendants,
k2DScaleTransformWithCompositedDescendants,
// This must be the last value because it's used in tracing code to know the
// number of reasons.
kTest,
@ -153,6 +153,12 @@ struct CC_EXPORT EffectNode {
// doesn't allow LCD text.
// This is set and used for the impl-side effect tree only.
bool lcd_text_disallowed_by_backdrop_filter : 1 = false;
// True if a backdrop effect may be present on this effect (and therefore
// any side-effects on ancestors should be taken into account).
bool may_have_backdrop_effect : 1 = false;
// Whether this effect is related to a non-identity 2D scale transform (and no
// other transform).
bool has_2d_scale_transform : 1 = false;
// RenderSurfaceReason::kNone if this effect node should not create a render
// surface, or the reason that this effect node should create one.
RenderSurfaceReason render_surface_reason = RenderSurfaceReason::kNone;

@ -1072,7 +1072,7 @@ void EffectTree::UpdateSurfaceContentsScale(EffectNode* effect_node) {
// To avoid seams we apply only scale as draw transform instead of raster
// content transform.
if (effect_node->render_surface_reason ==
RenderSurfaceReason::k2DTransformWithCompositedDescendants) {
RenderSurfaceReason::k2DScaleTransformWithCompositedDescendants) {
// We raster at closest positive integer scale and then apply the rest as
// the draw transform, e.g scale 3.5 will rastered at 4 and 0.875 (3.5/4)
// will be applied as draw transform.

@ -108,12 +108,7 @@ CompositingReasons CompositingReasonsFor3DTransform(
CompositingReasons reasons =
CompositingReasonFinder::PotentialCompositingReasonsFor3DTransform(style);
bool has_scale =
RuntimeEnabledFeatures::RenderSurfaceForScaleTransformEnabled() &&
style.Scale();
if ((reasons != CompositingReason::kNone || has_scale) &&
layout_object.IsBox()) {
if (reasons != CompositingReason::kNone && layout_object.IsBox()) {
// In theory this should operate on fragment sizes, but using the box size
// is probably good enough for a use counter.
auto& box = To<LayoutBox>(layout_object);
@ -135,14 +130,6 @@ CompositingReasons CompositingReasonsFor3DTransform(
WebFeature::kTransform3dScene);
}
}
if (has_scale) {
// TODO(chrishtr): make this "2d scale with composited descendants"
// somehow. Currently it triggers for all elements with
// the scale CSS property (*not* transform--just scale, which is
// a shorthand for scale transforms).
reasons |= CompositingReason::k2DScaleTransformWithCompositedDescendants;
}
}
return reasons;
}

@ -469,7 +469,8 @@ TEST_P(PaintAndRasterInvalidationTest, NonCompositedLayoutViewResize) {
TEST_P(PaintAndRasterInvalidationTest, FullInvalidationWithHTMLTransform) {
GetDocument().documentElement()->setAttribute(
html_names::kStyleAttr, AtomicString("transform: scale(0.5)"));
html_names::kStyleAttr,
AtomicString("transform: scale(0.5) translateX(1px)"));
const DisplayItemClient& client = ViewScrollingBackgroundClient();
UpdateAllLifecyclePhasesForTest();
@ -477,6 +478,7 @@ TEST_P(PaintAndRasterInvalidationTest, FullInvalidationWithHTMLTransform) {
GetDocument().View()->Resize(gfx::Size(500, 500));
UpdateAllLifecyclePhasesForTest();
ASSERT_TRUE(GetRasterInvalidationTracking());
EXPECT_THAT(
GetRasterInvalidationTracking()->Invalidations(),
UnorderedElementsAre(

@ -261,6 +261,7 @@ class FragmentPaintPropertyTreeBuilder {
ALWAYS_INLINE void UpdateTransform();
ALWAYS_INLINE void UpdateTransformForSVGChild(CompositingReasons);
ALWAYS_INLINE bool NeedsEffect() const;
ALWAYS_INLINE bool NeedsEffectFor2DScaleTransform() const;
ALWAYS_INLINE bool EffectCanUseCurrentClipAsOutputClip() const;
ALWAYS_INLINE void UpdateViewTransitionSubframeRootEffect();
ALWAYS_INLINE void UpdateViewTransitionEffect();
@ -422,6 +423,12 @@ class FragmentPaintPropertyTreeBuilder {
// These are updated in UpdateClipPathClip() and used in UpdateEffect() if
// needs_mask_base_clip_path_ is true.
bool needs_mask_based_clip_path_ = false;
// True if, among all transform-relaed properties, there is a
// non-identity transform that *is* a 2D scale.
bool has_scale2d_transform_ = false;
// True if, among all transform-relaed properties, there is a
// non-identity transform that *is not* a 2D scale.
bool has_non_scale2d_transform_ = false;
std::optional<gfx::RectF> clip_path_bounding_box_;
};
@ -1239,6 +1246,15 @@ void FragmentPaintPropertyTreeBuilder::UpdateIndividualTransform(
state.transform_and_origin =
TransformAndOriginState(box, reference_box, compute_matrix);
has_non_scale2d_transform_ =
has_non_scale2d_transform_ ||
(!state.transform_and_origin.matrix.IsScale2d() &&
!state.transform_and_origin.matrix.IsIdentity());
has_scale2d_transform_ =
has_scale2d_transform_ ||
(state.transform_and_origin.matrix.IsScale2d() &&
!state.transform_and_origin.matrix.IsIdentity());
// If a node with transform-style: preserve-3d does not exist in an
// existing rendering context, it establishes a new one.
state.rendering_context_id = context_.rendering_context_id;
@ -1479,7 +1495,18 @@ static bool NeedsEffectForViewTransition(const LayoutObject& object) {
!object.IsLayoutView();
}
static bool NeedsEffectIgnoringClipPath(
// Scale transforms need effects, so that they can be considered for
// promotion to render surfaces if possible to improve quality of
// renerdering. See crbug.com/40084005.
bool FragmentPaintPropertyTreeBuilder::NeedsEffectFor2DScaleTransform() const {
if (!RuntimeEnabledFeatures::RenderSurfaceFor2DScaleTransformEnabled() ||
object_.IsLayoutReplaced()) {
return false;
}
return has_scale2d_transform_ && !has_non_scale2d_transform_;
}
static bool NeedsEffectIgnoringClipPathAnd2DScale(
const LayoutObject& object,
CompositingReasons direct_compositing_reasons) {
if (object.IsText()) {
@ -1539,12 +1566,6 @@ static bool NeedsEffectIgnoringClipPath(
return true;
}
if (RuntimeEnabledFeatures::RenderSurfaceForScaleTransformEnabled() &&
(direct_compositing_reasons &
CompositingReason::k2DScaleTransformWithCompositedDescendants)) {
return true;
}
return false;
}
@ -1553,8 +1574,11 @@ bool FragmentPaintPropertyTreeBuilder::NeedsEffect() const {
// A mask-based clip-path needs an effect node, similar to a normal mask.
if (needs_mask_based_clip_path_)
return true;
return NeedsEffectIgnoringClipPath(object_,
full_context_.direct_compositing_reasons);
if (NeedsEffectFor2DScaleTransform()) {
return true;
}
return NeedsEffectIgnoringClipPathAnd2DScale(
object_, full_context_.direct_compositing_reasons);
}
// An effect node can use the current clip as its output clip if the clip won't
@ -1656,6 +1680,8 @@ void FragmentPaintPropertyTreeBuilder::UpdateEffect() {
}
}
state.has_2d_scale_transform = NeedsEffectFor2DScaleTransform();
state.direct_compositing_reasons =
full_context_.direct_compositing_reasons &
CompositingReason::kDirectReasonsForEffectProperty;
@ -1666,10 +1692,6 @@ void FragmentPaintPropertyTreeBuilder::UpdateEffect() {
(full_context_.direct_compositing_reasons &
CompositingReason::kAdditionalEffectCompositingTrigger);
state.direct_compositing_reasons |=
(full_context_.direct_compositing_reasons &
CompositingReason::k2DScaleTransformWithCompositedDescendants);
// We may begin to composite our subtree prior to an animation starts, but
// a compositor element ID is only needed when an animation is current.
// Currently, we use the existence of this id to check if effect nodes
@ -3447,8 +3469,8 @@ void PaintPropertyTreeBuilder::InitPaintProperties() {
NeedsScale(object_, context_.direct_compositing_reasons) ||
NeedsOffset(object_, context_.direct_compositing_reasons) ||
NeedsTransform(object_, context_.direct_compositing_reasons) ||
NeedsEffectIgnoringClipPath(object_,
context_.direct_compositing_reasons) ||
NeedsEffectIgnoringClipPathAnd2DScale(
object_, context_.direct_compositing_reasons) ||
NeedsClipPathClipOrMask(object_) ||
NeedsTransformForSVGChild(object_,
context_.direct_compositing_reasons) ||
@ -3790,7 +3812,7 @@ bool PaintPropertyTreeBuilder::ScheduleDeferredOpacityNodeUpdate(
return false;
}
// Fast-path for directly updating transforms. Returns true if successful. This
// Fast-path for directly updating transforms. This
// is similar to |FragmentPaintPropertyTreeBuilder::UpdateIndividualTransform|.
void PaintPropertyTreeBuilder::DirectlyUpdateTransformMatrix(
const LayoutObject& object) {

@ -1263,10 +1263,6 @@ static cc::RenderSurfaceReason ConditionalRenderSurfaceReasonForEffect(
static cc::RenderSurfaceReason RenderSurfaceReasonForEffect(
const EffectPaintPropertyNode& effect) {
if (effect
.RequiresCompositingFor2DScaleTransformWithCompositedDescendants()) {
return cc::RenderSurfaceReason::k2DTransformWithCompositedDescendants;
}
if (!effect.Filter().IsEmpty() ||
effect.RequiresCompositingForWillChangeFilter()) {
return cc::RenderSurfaceReason::kFilter;
@ -1313,7 +1309,9 @@ void PropertyTreeManager::PopulateCcEffectNode(
effect_node.opacity = effect.Opacity();
const auto& transform = effect.LocalTransformSpace().Unalias();
effect_node.transform_id = EnsureCompositorTransformNode(transform);
effect_node.has_2d_scale_transform = effect.Has2DScaleTransform();
if (effect.MayHaveBackdropEffect()) {
effect_node.may_have_backdrop_effect = true;
// We never have backdrop effect and filter on the same effect node.
DCHECK(effect.Filter().IsEmpty());
if (auto* backdrop_filter = effect.BackdropFilter()) {
@ -1342,6 +1340,7 @@ void PropertyTreeManager::UpdateConditionalRenderSurfaceReasons(
wtf_size_t tree_size = base::checked_cast<wtf_size_t>(effect_tree_.size());
Vector<int> effect_layer_counts(tree_size);
Vector<bool> has_child_surface(tree_size);
Vector<bool> has_backdrop_effect_descendant(tree_size);
// Initialize the vector to count directly controlled layers.
for (const auto& layer : layers) {
if (layer->draws_content())
@ -1353,6 +1352,16 @@ void PropertyTreeManager::UpdateConditionalRenderSurfaceReasons(
// effect_layer_counts.
for (int id = tree_size - 1; id > cc::kSecondaryRootPropertyNodeId; id--) {
auto* effect = effect_tree_.Node(id);
if (RuntimeEnabledFeatures::RenderSurfaceFor2DScaleTransformEnabled() &&
effect->render_surface_reason == cc::RenderSurfaceReason::kNone &&
!has_backdrop_effect_descendant[id] &&
!effect->may_have_backdrop_effect && effect->has_2d_scale_transform &&
effect_layer_counts[id] >= 2) {
effect->render_surface_reason =
cc::RenderSurfaceReason::k2DScaleTransformWithCompositedDescendants;
}
if (effect_layer_counts[id] < 2 &&
IsConditionalRenderSurfaceReason(effect->render_surface_reason) &&
// kBlendModeDstIn should create a render surface if the mask itself
@ -1377,6 +1386,12 @@ void PropertyTreeManager::UpdateConditionalRenderSurfaceReasons(
has_child_surface[effect->parent_id] |= has_child_surface[id];
}
if (effect->parent_id != cc::kInvalidPropertyNodeId &&
(effect->may_have_backdrop_effect ||
has_backdrop_effect_descendant[id])) {
has_backdrop_effect_descendant[effect->parent_id] = true;
}
#if DCHECK_IS_ON()
// Mark we have visited this effect.
effect_layer_counts[id] = -1;

@ -114,8 +114,6 @@ constexpr auto kReasonDescriptionMap = std::to_array<ReasonAndDescription>({
{CompositingReason::kDevToolsOverlay, "Is DevTools overlay."},
{CompositingReason::kViewTransitionContent,
"The layer containing the contents of a view transition element."},
{CompositingReason::k2DScaleTransformWithCompositedDescendants,
"Has a 2D scale transform with composited descendants."},
});
} // anonymous namespace

@ -90,7 +90,6 @@ using CompositingReasons = uint64_t;
V(LinkHighlight) \
V(DevToolsOverlay) \
V(ViewTransitionContent) \
V(2DScaleTransformWithCompositedDescendants)
class PLATFORM_EXPORT CompositingReason {
DISALLOW_NEW();

@ -135,6 +135,8 @@ class PLATFORM_EXPORT EffectPaintPropertyNode final
// propagated to the subtree of the effect tree.
bool self_or_ancestor_participates_in_view_transition = false;
bool has_2d_scale_transform = false;
PaintPropertyChangeType ComputeChange(
const State& other,
const AnimationState& animation_state) const;
@ -243,10 +245,6 @@ class PLATFORM_EXPORT EffectPaintPropertyNode final
bool HasDirectCompositingReasons() const {
return state_.direct_compositing_reasons != CompositingReason::kNone;
}
bool RequiresCompositingFor2DScaleTransformWithCompositedDescendants() const {
return state_.direct_compositing_reasons &
CompositingReason::k2DScaleTransformWithCompositedDescendants;
}
bool RequiresCompositingForBackdropFilterMask() const {
return state_.direct_compositing_reasons &
CompositingReason::kBackdropFilterMask;
@ -338,6 +336,8 @@ class PLATFORM_EXPORT EffectPaintPropertyNode final
return state_.self_or_ancestor_participates_in_view_transition;
}
bool Has2DScaleTransform() const { return state_.has_2d_scale_transform; }
std::unique_ptr<JSONObject> ToJSON() const final;
// These are public required by MakeGarbageCollected, but the protected tags

@ -3626,7 +3626,8 @@
// Allocates a render surface for 2D scale transforms, to prevent
// composited pixel alignment issues. See
// https://crbug.com/40084005
name: "RenderSurfaceForScaleTransform",
name: "RenderSurfaceFor2DScaleTransform",
status: "experimental",
},
{
name: "ReportEventTimingAtVisibilityChange",

Binary file not shown.

Before

(image error) Size: 91 KiB

After

(image error) Size: 96 KiB

@ -9,6 +9,7 @@
<meta name="assert" content='Tests that applying multiple transforms to an
element is the same as applying the transforms in the same order to nested
elements.'>
<meta name=fuzzy content="maxDifference=0-30;totalPixels=0-500">
<link rel="match" href="transform-compound-ref.html">
<link rel="mismatch" href="transform-compound-notref-1.html">
<link rel="mismatch" href="transform-compound-notref-2.html">
@ -28,7 +29,6 @@
background-color: gold;
width: 200px;
height: 100px;
border: 1px solid black;
transform: translate(100px) scale(2) rotate(90deg) skewX(15deg);
}
</style>

@ -21,7 +21,6 @@
background-color: gold;
width: 200px;
height: 100px;
border: 1px solid black;
}
</style>
</head>

@ -1,5 +1,6 @@
<!doctype html>
<title>foreignObject with scale transform and overflow:scroll</title>
<meta name=fuzzy content="maxDifference=0-10;totalPixels=0-10">
<link rel="help" href="https://svgwg.org/svg2-draft/single-page.html#embedded-ForeignObjectElement"/>
<link rel="match" href="foreign-object-scale-scroll-ref.html">
<svg width="400" height="400">

Binary file not shown.

Before

(image error) Size: 3.2 KiB

After

(image error) Size: 3.2 KiB

Binary file not shown.

Before

(image error) Size: 5.6 KiB

After

(image error) Size: 5.4 KiB

Binary file not shown.

Before

(image error) Size: 2.8 KiB

After

(image error) Size: 2.8 KiB

Binary file not shown.

Before

(image error) Size: 2.8 KiB

After

(image error) Size: 2.8 KiB

Binary file not shown.

After

(image error) Size: 5.6 KiB

Binary file not shown.

After

(image error) Size: 5.5 KiB

Binary file not shown.

After

(image error) Size: 23 KiB

Binary file not shown.

After

(image error) Size: 2.8 KiB

Binary file not shown.

After

(image error) Size: 2.8 KiB

Binary file not shown.

After

(image error) Size: 3.3 KiB

@ -0,0 +1,175 @@
{
"layers": [
{
"name": "Scrolling background of LayoutView #document",
"bounds": [800, 600],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF"
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child1' class='child composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 2
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child2' class='child scale composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 4
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child3' class='child'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 6
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child4' class='child scale'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 8
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child5' class='child composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 10
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child6' class='child scale composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 12
}
],
"transforms": [
{
"id": 1,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 8, 0, 1]
]
},
{
"id": 2,
"parent": 1,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 3,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[9, 58, 0, 1]
]
},
{
"id": 4,
"parent": 3,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 5,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 108, 0, 1]
]
},
{
"id": 6,
"parent": 5,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 7,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 158, 0, 1]
]
},
{
"id": 8,
"parent": 7,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 9,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 208, 0, 1]
]
},
{
"id": 10,
"parent": 9,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 11,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 258, 0, 1]
]
},
{
"id": 12,
"parent": 11,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
}
]
}

@ -0,0 +1,56 @@
{
"layers": [
{
"name": "Scrolling background of LayoutView #document",
"bounds": [785, 2016],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF",
"transform": 1
},
{
"name": "LayoutBlockFlow (positioned) DIV id='t' class='green translated'",
"bounds": [50, 50],
"contentsOpaque": true,
"backgroundColor": "#008000",
"transform": 3
},
{
"name": "VerticalScrollbar",
"position": [785, 0],
"bounds": [15, 600],
"contentsOpaque": true
}
],
"transforms": [
{
"id": 1,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, -200, 0, 1]
]
},
{
"id": 2,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[125, 125, 0, 1]
]
},
{
"id": 3,
"parent": 2,
"transform": [
[2, 0, 0, 0],
[0, 2, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [25, 25]
}
]
}

@ -0,0 +1,14 @@
{
"layers": [
{
"name": "Scrolling background of LayoutView #document",
"bounds": [800, 600],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF",
"invalidations": [
[85, 70, 91, 91]
]
}
]
}

Binary file not shown.

After

(image error) Size: 12 KiB

Binary file not shown.

After

(image error) Size: 14 KiB

@ -0,0 +1,175 @@
{
"layers": [
{
"name": "Scrolling background of LayoutView #document",
"bounds": [800, 600],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF"
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child1' class='child composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 2
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child2' class='child scale composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 4
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child3' class='child'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 6
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child4' class='child scale'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 8
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child5' class='child composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 10
},
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child6' class='child scale composited'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 12
}
],
"transforms": [
{
"id": 1,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 8, 0, 1]
]
},
{
"id": 2,
"parent": 1,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 3,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[9, 58, 0, 1]
]
},
{
"id": 4,
"parent": 3,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 5,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 108, 0, 1]
]
},
{
"id": 6,
"parent": 5,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 7,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 158, 0, 1]
]
},
{
"id": 8,
"parent": 7,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 9,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 208, 0, 1]
]
},
{
"id": 10,
"parent": 9,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
},
{
"id": 11,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 258, 0, 1]
]
},
{
"id": 12,
"parent": 11,
"transform": [
[40, 0, 0, 0],
[0, 40, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [0, 0]
}
]
}

@ -0,0 +1,56 @@
{
"layers": [
{
"name": "Scrolling background of LayoutView #document",
"bounds": [785, 2016],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF",
"transform": 1
},
{
"name": "LayoutBlockFlow (positioned) DIV id='t' class='green translated'",
"bounds": [50, 50],
"contentsOpaque": true,
"backgroundColor": "#008000",
"transform": 3
},
{
"name": "VerticalScrollbar",
"position": [785, 0],
"bounds": [15, 600],
"contentsOpaque": true
}
],
"transforms": [
{
"id": 1,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, -200, 0, 1]
]
},
{
"id": 2,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[125, 125, 0, 1]
]
},
{
"id": 3,
"parent": 2,
"transform": [
[2, 0, 0, 0],
[0, 2, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [25, 25]
}
]
}

Binary file not shown.

Before

(image error) Size: 84 KiB

After

(image error) Size: 89 KiB

@ -30,7 +30,6 @@
{
"name": "LayoutBlockFlow (relative positioned) DIV id='child4' class='child scale'",
"bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF",
"transform": 8
},

@ -10,8 +10,10 @@
{
"name": "LayoutBlockFlow (positioned) DIV id='t' class='green translated'",
"bounds": [50, 50],
"contentsOpaque": true,
"backgroundColor": "#008000",
"invalidations": [
[0, 0, 50, 50]
],
"transform": 3
},
{

Binary file not shown.

Before

(image error) Size: 2.8 KiB

After

(image error) Size: 2.8 KiB

@ -4,10 +4,43 @@
"name": "Scrolling background of LayoutView #document",
"bounds": [800, 600],
"contentsOpaque": true,
"backgroundColor": "#FFFFFF",
"invalidations": [
[85, 70, 91, 91]
"backgroundColor": "#FFFFFF"
},
{
"name": "LayoutBlockFlow (children-inline) DIV id='zoom' class='zoom-div'",
"bounds": [784, 46],
"drawsContent": false,
"transform": 2
},
{
"name": "LayoutBlockFlow (positioned) DIV id='target'",
"position": [42, 67],
"bounds": [100, 100],
"contentsOpaque": true,
"backgroundColor": "#D3D3D3",
"transform": 2
}
],
"transforms": [
{
"id": 1,
"transform": [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[8, 8, 0, 1]
]
},
{
"id": 2,
"parent": 1,
"transform": [
[0.899999976158142, 0, 0, 0],
[0, 0.899999976158142, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
],
"origin": [392, 22.75]
}
]
}

@ -2,7 +2,7 @@
<style>
.fakeSVG {
transform-origin: center;
transform: scale(2.0);
transform: scale(2.0) translateX(1px);
height: 150px;
width: 300px;
border: 2px solid red;

@ -2,7 +2,7 @@
<style>
svg {
transform-origin: center;
transform: scale(2.0);
transform: scale(2.0) translateX(1px);
border: 2px solid red;
}
div {

Binary file not shown.

Before

(image error) Size: 8.5 KiB

After

(image error) Size: 8.0 KiB

Binary file not shown.

Before

(image error) Size: 7.0 KiB

After

(image error) Size: 6.8 KiB

Binary file not shown.

Before

(image error) Size: 26 KiB

After

(image error) Size: 25 KiB

Binary file not shown.

Before

(image error) Size: 40 KiB

After

(image error) Size: 39 KiB

Binary file not shown.

Before

(image error) Size: 5.6 KiB

After

(image error) Size: 5.4 KiB

Binary file not shown.

Before

(image error) Size: 5.5 KiB

After

(image error) Size: 5.4 KiB

Binary file not shown.

Before

(image error) Size: 53 KiB

After

(image error) Size: 53 KiB

Binary file not shown.

Before

(image error) Size: 23 KiB

After

(image error) Size: 23 KiB

Binary file not shown.

Before

(image error) Size: 3.3 KiB

After

(image error) Size: 3.2 KiB

Binary file not shown.

Before

(image error) Size: 3.1 KiB

After

(image error) Size: 3.0 KiB

Binary file not shown.

Before

(image error) Size: 12 KiB

After

(image error) Size: 12 KiB

Binary file not shown.

Before

(image error) Size: 14 KiB

After

(image error) Size: 14 KiB

Binary file not shown.

Before

(image error) Size: 3.3 KiB

After

(image error) Size: 3.3 KiB

Binary file not shown.

Before

(image error) Size: 3.1 KiB

After

(image error) Size: 3.1 KiB

Binary file not shown.

Before

(image error) Size: 5.5 KiB

After

(image error) Size: 5.4 KiB

Binary file not shown.

Before

(image error) Size: 53 KiB

After

(image error) Size: 53 KiB

Binary file not shown.

Before

(image error) Size: 23 KiB

After

(image error) Size: 23 KiB

Binary file not shown.

Before

(image error) Size: 3.1 KiB

After

(image error) Size: 3.1 KiB

Binary file not shown.

Before

(image error) Size: 39 KiB

After

(image error) Size: 38 KiB

Binary file not shown.

Before

(image error) Size: 3.1 KiB

After

(image error) Size: 3.1 KiB

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<div style="position: relative; left: 27px; width: 100px;">
<div style="width: 50px;">
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="pattern" width="100" height="100" patternUnits="userSpaceOnUse">
<circle cx="50" cy="50" r="50" fill="green"/>
</pattern>
<mask id="mask">
<circle cx="50" cy="50" r="50" fill="white"/>
</mask>
<clipPath id="clip">
<circle cx="50" cy="50" r="50"/>
<circle cx="50" cy="50" r="50"/>
</clipPath>
<filter id="filter">
<feOffset dx="0" dy="0"/>
</filter>
</defs>
<circle cx="50" cy="50" r="50" fill="green"/>
<g transform="translate(300)">
<rect width="100" height="100" fill="url(#pattern)"></rect>
</g>
<g transform="translate(150 150)">
<rect width="100" height="100" fill="green" mask="url(#mask)"/>
</g>
<g transform="translate(0 300)">
<rect width="100" height="100" fill="green" clip-path="url(#clip)"/>
</g>
<g transform="translate(300 300)">
<circle cx="50" cy="50" r="50" fill="green" filter="url(#filter)"/>
</g>
</svg>
</div>
</div>
</body>
</html>

Binary file not shown.

After

(image error) Size: 11 KiB

Binary file not shown.

After

(image error) Size: 2.9 KiB

@ -0,0 +1,20 @@
<!DOCTYPE html>
<style>
.black-layer {
width: 100px;
height: 100px;
background: black;
will-change: opacity;
backdrop-filter: blur(1px);
}
#parent {
transform-origin:0 0;
scale: 0.995;
}
</style>
<!-- Tests that adding backdrop filter causes 1px seams due to incompatibility
of our heuristics and that feature. -->
<div id=parent>
<div class="black-layer"></div>
<div class="black-layer"></div>
</div>

@ -0,0 +1,20 @@
<!DOCTYPE html>
<style>
.black-layer {
width: 100px;
height: 100px;
background: black;
will-change: opacity;
}
#parent {
transform-origin:0 0;
scale: 0.995;
}
</style>
<!-- Tests that there is no 1px seam between the two .black-layer composited
layers. -->
<div id=parent>
<div class="black-layer"></div>
<div class="black-layer"></div>
</div>

@ -1,5 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="icb-scaling-005-print-ref.html">
<meta name=fuzzy content="maxDifference=0-5;totalPixels=0-20">
<style>
@page {
size: 300px;

@ -1,5 +1,6 @@
<!DOCTYPE html>
<link rel="match" href="page-fitting-004-print-ref.html">
<meta name=fuzzy content="maxDifference=0-255;totalPixels=0-10">
<script>
if (window.testRunner) {
testRunner.setPrintingSize(300, 400);