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"; return "subtree being captured";
case RenderSurfaceReason::kViewTransitionParticipant: case RenderSurfaceReason::kViewTransitionParticipant:
return "view transition participant"; return "view transition participant";
case RenderSurfaceReason::k2DTransformWithCompositedDescendants: case RenderSurfaceReason::k2DScaleTransformWithCompositedDescendants:
return "2D transform with composited descendants"; return "2D scale transform with composited descendants";
case RenderSurfaceReason::kTest: case RenderSurfaceReason::kTest:
return "test"; return "test";
} }

@ -53,7 +53,7 @@ enum class RenderSurfaceReason : uint8_t {
kSubtreeIsBeingCaptured, kSubtreeIsBeingCaptured,
kViewTransitionParticipant, kViewTransitionParticipant,
kGradientMask, kGradientMask,
k2DTransformWithCompositedDescendants, k2DScaleTransformWithCompositedDescendants,
// This must be the last value because it's used in tracing code to know the // This must be the last value because it's used in tracing code to know the
// number of reasons. // number of reasons.
kTest, kTest,
@ -153,6 +153,12 @@ struct CC_EXPORT EffectNode {
// doesn't allow LCD text. // doesn't allow LCD text.
// This is set and used for the impl-side effect tree only. // This is set and used for the impl-side effect tree only.
bool lcd_text_disallowed_by_backdrop_filter : 1 = false; 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 // RenderSurfaceReason::kNone if this effect node should not create a render
// surface, or the reason that this effect node should create one. // surface, or the reason that this effect node should create one.
RenderSurfaceReason render_surface_reason = RenderSurfaceReason::kNone; 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 // To avoid seams we apply only scale as draw transform instead of raster
// content transform. // content transform.
if (effect_node->render_surface_reason == if (effect_node->render_surface_reason ==
RenderSurfaceReason::k2DTransformWithCompositedDescendants) { RenderSurfaceReason::k2DScaleTransformWithCompositedDescendants) {
// We raster at closest positive integer scale and then apply the rest as // 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) // the draw transform, e.g scale 3.5 will rastered at 4 and 0.875 (3.5/4)
// will be applied as draw transform. // will be applied as draw transform.

@ -108,12 +108,7 @@ CompositingReasons CompositingReasonsFor3DTransform(
CompositingReasons reasons = CompositingReasons reasons =
CompositingReasonFinder::PotentialCompositingReasonsFor3DTransform(style); CompositingReasonFinder::PotentialCompositingReasonsFor3DTransform(style);
bool has_scale = if (reasons != CompositingReason::kNone && layout_object.IsBox()) {
RuntimeEnabledFeatures::RenderSurfaceForScaleTransformEnabled() &&
style.Scale();
if ((reasons != CompositingReason::kNone || has_scale) &&
layout_object.IsBox()) {
// In theory this should operate on fragment sizes, but using the box size // In theory this should operate on fragment sizes, but using the box size
// is probably good enough for a use counter. // is probably good enough for a use counter.
auto& box = To<LayoutBox>(layout_object); auto& box = To<LayoutBox>(layout_object);
@ -135,14 +130,6 @@ CompositingReasons CompositingReasonsFor3DTransform(
WebFeature::kTransform3dScene); 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; return reasons;
} }

@ -469,7 +469,8 @@ TEST_P(PaintAndRasterInvalidationTest, NonCompositedLayoutViewResize) {
TEST_P(PaintAndRasterInvalidationTest, FullInvalidationWithHTMLTransform) { TEST_P(PaintAndRasterInvalidationTest, FullInvalidationWithHTMLTransform) {
GetDocument().documentElement()->setAttribute( 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(); const DisplayItemClient& client = ViewScrollingBackgroundClient();
UpdateAllLifecyclePhasesForTest(); UpdateAllLifecyclePhasesForTest();
@ -477,6 +478,7 @@ TEST_P(PaintAndRasterInvalidationTest, FullInvalidationWithHTMLTransform) {
GetDocument().View()->Resize(gfx::Size(500, 500)); GetDocument().View()->Resize(gfx::Size(500, 500));
UpdateAllLifecyclePhasesForTest(); UpdateAllLifecyclePhasesForTest();
ASSERT_TRUE(GetRasterInvalidationTracking());
EXPECT_THAT( EXPECT_THAT(
GetRasterInvalidationTracking()->Invalidations(), GetRasterInvalidationTracking()->Invalidations(),
UnorderedElementsAre( UnorderedElementsAre(

@ -261,6 +261,7 @@ class FragmentPaintPropertyTreeBuilder {
ALWAYS_INLINE void UpdateTransform(); ALWAYS_INLINE void UpdateTransform();
ALWAYS_INLINE void UpdateTransformForSVGChild(CompositingReasons); ALWAYS_INLINE void UpdateTransformForSVGChild(CompositingReasons);
ALWAYS_INLINE bool NeedsEffect() const; ALWAYS_INLINE bool NeedsEffect() const;
ALWAYS_INLINE bool NeedsEffectFor2DScaleTransform() const;
ALWAYS_INLINE bool EffectCanUseCurrentClipAsOutputClip() const; ALWAYS_INLINE bool EffectCanUseCurrentClipAsOutputClip() const;
ALWAYS_INLINE void UpdateViewTransitionSubframeRootEffect(); ALWAYS_INLINE void UpdateViewTransitionSubframeRootEffect();
ALWAYS_INLINE void UpdateViewTransitionEffect(); ALWAYS_INLINE void UpdateViewTransitionEffect();
@ -422,6 +423,12 @@ class FragmentPaintPropertyTreeBuilder {
// These are updated in UpdateClipPathClip() and used in UpdateEffect() if // These are updated in UpdateClipPathClip() and used in UpdateEffect() if
// needs_mask_base_clip_path_ is true. // needs_mask_base_clip_path_ is true.
bool needs_mask_based_clip_path_ = false; 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_; std::optional<gfx::RectF> clip_path_bounding_box_;
}; };
@ -1239,6 +1246,15 @@ void FragmentPaintPropertyTreeBuilder::UpdateIndividualTransform(
state.transform_and_origin = state.transform_and_origin =
TransformAndOriginState(box, reference_box, compute_matrix); 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 // If a node with transform-style: preserve-3d does not exist in an
// existing rendering context, it establishes a new one. // existing rendering context, it establishes a new one.
state.rendering_context_id = context_.rendering_context_id; state.rendering_context_id = context_.rendering_context_id;
@ -1479,7 +1495,18 @@ static bool NeedsEffectForViewTransition(const LayoutObject& object) {
!object.IsLayoutView(); !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, const LayoutObject& object,
CompositingReasons direct_compositing_reasons) { CompositingReasons direct_compositing_reasons) {
if (object.IsText()) { if (object.IsText()) {
@ -1539,12 +1566,6 @@ static bool NeedsEffectIgnoringClipPath(
return true; return true;
} }
if (RuntimeEnabledFeatures::RenderSurfaceForScaleTransformEnabled() &&
(direct_compositing_reasons &
CompositingReason::k2DScaleTransformWithCompositedDescendants)) {
return true;
}
return false; return false;
} }
@ -1553,8 +1574,11 @@ bool FragmentPaintPropertyTreeBuilder::NeedsEffect() const {
// A mask-based clip-path needs an effect node, similar to a normal mask. // A mask-based clip-path needs an effect node, similar to a normal mask.
if (needs_mask_based_clip_path_) if (needs_mask_based_clip_path_)
return true; return true;
return NeedsEffectIgnoringClipPath(object_, if (NeedsEffectFor2DScaleTransform()) {
full_context_.direct_compositing_reasons); 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 // 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 = state.direct_compositing_reasons =
full_context_.direct_compositing_reasons & full_context_.direct_compositing_reasons &
CompositingReason::kDirectReasonsForEffectProperty; CompositingReason::kDirectReasonsForEffectProperty;
@ -1666,10 +1692,6 @@ void FragmentPaintPropertyTreeBuilder::UpdateEffect() {
(full_context_.direct_compositing_reasons & (full_context_.direct_compositing_reasons &
CompositingReason::kAdditionalEffectCompositingTrigger); 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 // 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. // 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 // 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) || NeedsScale(object_, context_.direct_compositing_reasons) ||
NeedsOffset(object_, context_.direct_compositing_reasons) || NeedsOffset(object_, context_.direct_compositing_reasons) ||
NeedsTransform(object_, context_.direct_compositing_reasons) || NeedsTransform(object_, context_.direct_compositing_reasons) ||
NeedsEffectIgnoringClipPath(object_, NeedsEffectIgnoringClipPathAnd2DScale(
context_.direct_compositing_reasons) || object_, context_.direct_compositing_reasons) ||
NeedsClipPathClipOrMask(object_) || NeedsClipPathClipOrMask(object_) ||
NeedsTransformForSVGChild(object_, NeedsTransformForSVGChild(object_,
context_.direct_compositing_reasons) || context_.direct_compositing_reasons) ||
@ -3790,7 +3812,7 @@ bool PaintPropertyTreeBuilder::ScheduleDeferredOpacityNodeUpdate(
return false; 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|. // is similar to |FragmentPaintPropertyTreeBuilder::UpdateIndividualTransform|.
void PaintPropertyTreeBuilder::DirectlyUpdateTransformMatrix( void PaintPropertyTreeBuilder::DirectlyUpdateTransformMatrix(
const LayoutObject& object) { const LayoutObject& object) {

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

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

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

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

@ -3626,7 +3626,8 @@
// Allocates a render surface for 2D scale transforms, to prevent // Allocates a render surface for 2D scale transforms, to prevent
// composited pixel alignment issues. See // composited pixel alignment issues. See
// https://crbug.com/40084005 // https://crbug.com/40084005
name: "RenderSurfaceForScaleTransform", name: "RenderSurfaceFor2DScaleTransform",
status: "experimental",
}, },
{ {
name: "ReportEventTimingAtVisibilityChange", 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 <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 element is the same as applying the transforms in the same order to nested
elements.'> elements.'>
<meta name=fuzzy content="maxDifference=0-30;totalPixels=0-500">
<link rel="match" href="transform-compound-ref.html"> <link rel="match" href="transform-compound-ref.html">
<link rel="mismatch" href="transform-compound-notref-1.html"> <link rel="mismatch" href="transform-compound-notref-1.html">
<link rel="mismatch" href="transform-compound-notref-2.html"> <link rel="mismatch" href="transform-compound-notref-2.html">
@ -28,7 +29,6 @@
background-color: gold; background-color: gold;
width: 200px; width: 200px;
height: 100px; height: 100px;
border: 1px solid black;
transform: translate(100px) scale(2) rotate(90deg) skewX(15deg); transform: translate(100px) scale(2) rotate(90deg) skewX(15deg);
} }
</style> </style>

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

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<title>foreignObject with scale transform and overflow:scroll</title> <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="help" href="https://svgwg.org/svg2-draft/single-page.html#embedded-ForeignObjectElement"/>
<link rel="match" href="foreign-object-scale-scroll-ref.html"> <link rel="match" href="foreign-object-scale-scroll-ref.html">
<svg width="400" height="400"> <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'", "name": "LayoutBlockFlow (relative positioned) DIV id='child4' class='child scale'",
"bounds": [1, 1], "bounds": [1, 1],
"contentsOpaque": true,
"backgroundColor": "#0000FF", "backgroundColor": "#0000FF",
"transform": 8 "transform": 8
}, },

@ -10,8 +10,10 @@
{ {
"name": "LayoutBlockFlow (positioned) DIV id='t' class='green translated'", "name": "LayoutBlockFlow (positioned) DIV id='t' class='green translated'",
"bounds": [50, 50], "bounds": [50, 50],
"contentsOpaque": true,
"backgroundColor": "#008000", "backgroundColor": "#008000",
"invalidations": [
[0, 0, 50, 50]
],
"transform": 3 "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", "name": "Scrolling background of LayoutView #document",
"bounds": [800, 600], "bounds": [800, 600],
"contentsOpaque": true, "contentsOpaque": true,
"backgroundColor": "#FFFFFF", "backgroundColor": "#FFFFFF"
"invalidations": [ },
[85, 70, 91, 91] {
"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> <style>
.fakeSVG { .fakeSVG {
transform-origin: center; transform-origin: center;
transform: scale(2.0); transform: scale(2.0) translateX(1px);
height: 150px; height: 150px;
width: 300px; width: 300px;
border: 2px solid red; border: 2px solid red;

@ -2,7 +2,7 @@
<style> <style>
svg { svg {
transform-origin: center; transform-origin: center;
transform: scale(2.0); transform: scale(2.0) translateX(1px);
border: 2px solid red; border: 2px solid red;
} }
div { 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> <!DOCTYPE html>
<link rel="match" href="icb-scaling-005-print-ref.html"> <link rel="match" href="icb-scaling-005-print-ref.html">
<meta name=fuzzy content="maxDifference=0-5;totalPixels=0-20">
<style> <style>
@page { @page {
size: 300px; size: 300px;

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