layout: Allow changes transform and filter to skip layout

Chaneges to `transform` and `filter`-related properties do not need a
new layout. Instead, they can simply cause a regeneration of the display
list. The one caveat is that any pre-existing compositor scroll tree
needs to be patched up with new reference frame values before being sent
to the compositor.

This should improve performance of `transform`-only changes, commonly
used during animations.

Co-authored-by: Oriol Brufau <obrufau@igalia.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-05-26 17:04:27 +02:00
parent 325ac9e416
commit 6431aba718
4 changed files with 122 additions and 118 deletions

24
Cargo.lock generated
View file

@ -6634,7 +6634,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.28.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"bitflags 2.9.1",
"cssparser",
@ -6929,7 +6929,7 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.4.1"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"serde",
"stable_deref_trait",
@ -7390,7 +7390,7 @@ dependencies = [
[[package]]
name = "stylo"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"app_units",
"arrayvec",
@ -7448,7 +7448,7 @@ dependencies = [
[[package]]
name = "stylo_atoms"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"string_cache",
"string_cache_codegen",
@ -7457,12 +7457,12 @@ dependencies = [
[[package]]
name = "stylo_config"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
[[package]]
name = "stylo_derive"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"darling",
"proc-macro2",
@ -7474,7 +7474,7 @@ dependencies = [
[[package]]
name = "stylo_dom"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"bitflags 2.9.1",
"stylo_malloc_size_of",
@ -7483,7 +7483,7 @@ dependencies = [
[[package]]
name = "stylo_malloc_size_of"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"app_units",
"cssparser",
@ -7500,12 +7500,12 @@ dependencies = [
[[package]]
name = "stylo_static_prefs"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
[[package]]
name = "stylo_traits"
version = "0.3.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"app_units",
"bitflags 2.9.1",
@ -7914,7 +7914,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "to_shmem"
version = "0.2.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"cssparser",
"servo_arc",
@ -7927,7 +7927,7 @@ dependencies = [
[[package]]
name = "to_shmem_derive"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-05-01#1707256dd825f14e98161a49fdd6749f8ca0d506"
source = "git+https://github.com/servo/stylo?rev=refs%2Fpull%2F190%2Fhead#f63374493723974a6ec303f30a647ca22c3ae07b"
dependencies = [
"darling",
"proc-macro2",

View file

@ -119,7 +119,7 @@ rustls-pemfile = "2.0"
rustls-pki-types = "1.12"
script_layout_interface = { path = "components/shared/script_layout" }
script_traits = { path = "components/shared/script" }
selectors = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
selectors = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
serde = "1.0.219"
serde_bytes = "0.11"
serde_json = "1.0"
@ -127,7 +127,7 @@ servo-media = { git = "https://github.com/servo/media" }
servo-media-dummy = { git = "https://github.com/servo/media" }
servo-media-gstreamer = { git = "https://github.com/servo/media" }
servo-tracing = { path = "components/servo_tracing" }
servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
servo_arc = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
smallbitvec = "2.6.0"
smallvec = "1.15"
snapshot = { path = "./components/shared/snapshot" }
@ -136,12 +136,12 @@ string_cache = "0.8"
string_cache_codegen = "0.5"
strum = "0.26"
strum_macros = "0.26"
stylo = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" }
stylo = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
stylo_atoms = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
stylo_config = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
stylo_dom = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
stylo_malloc_size_of = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
stylo_traits = { git = "https://github.com/servo/stylo", rev = "refs/pull/190/head" }
surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] }
syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] }
synstructure = "0.13"

View file

@ -14,8 +14,8 @@ use compositing_traits::display_list::{
AxesScrollSensitivity, CompositorDisplayListInfo, ReferenceFrameNodeInfo, ScrollableNodeInfo,
SpatialTreeNodeInfo, StickyNodeInfo,
};
use euclid::SideOffsets2D;
use euclid::default::{Point2D, Rect, Size2D};
use euclid::{SideOffsets2D, Vector2D};
use log::warn;
use servo_config::opts::DebugOptions;
use style::Zero;
@ -33,8 +33,7 @@ use style::values::generics::transform::{self, GenericRotate, GenericScale, Gene
use style::values::specified::box_::DisplayOutside;
use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVector2D};
use webrender_api::{self as wr, BorderRadius};
use wr::StickyOffsetBounds;
use wr::units::{LayoutPixel, LayoutSize};
use wr::units::LayoutSize;
use super::ClipId;
use super::clip::StackingContextTreeClipStore;
@ -113,6 +112,10 @@ pub(crate) struct StackingContextTree {
/// for things like `overflow`. More clips may be created later during WebRender
/// display list construction, but they are never added here.
pub clip_store: StackingContextTreeClipStore,
/// A vector of `Fragment`s that created spatial nodes in the Compositor's
/// `ScrollTree`. This is used to patch up spatial nodes during repaint-only layouts.
pub fragments_creating_spatial_nodes: Vec<Option<Fragment>>,
}
impl StackingContextTree {
@ -168,6 +171,10 @@ impl StackingContextTree {
root_stacking_context: StackingContext::create_root(root_scroll_node_id, debug),
compositor_info,
clip_store: Default::default(),
// We are adding two empty slots here, because the Compositor `ScrollTree`
// adds two nodes for the root. These should never be patched during repaint
// only layouts.
fragments_creating_spatial_nodes: vec![None, None],
};
let mut root_stacking_context = StackingContext::create_root(root_scroll_node_id, debug);
@ -192,62 +199,35 @@ impl StackingContextTree {
stacking_context_tree
}
fn push_reference_frame(
&mut self,
origin: LayoutPoint,
parent_scroll_node_id: &ScrollTreeNodeId,
transform_style: wr::TransformStyle,
transform: LayoutTransform,
kind: wr::ReferenceFrameKind,
) -> ScrollTreeNodeId {
self.compositor_info.scroll_tree.add_scroll_tree_node(
Some(parent_scroll_node_id),
SpatialTreeNodeInfo::ReferenceFrame(ReferenceFrameNodeInfo {
origin,
transform_style,
transform,
kind,
}),
)
}
pub(crate) fn repair_scroll_tree_for_repaint_only_layout(&mut self) {
for (fragment, node) in self
.fragments_creating_spatial_nodes
.iter()
.zip(self.compositor_info.scroll_tree.nodes.iter_mut())
{
let box_fragment = match fragment {
Some(Fragment::Box(box_fragment) | Fragment::Float(box_fragment)) => box_fragment,
_ => continue,
};
fn define_scroll_frame(
&mut self,
parent_scroll_node_id: &ScrollTreeNodeId,
external_id: wr::ExternalScrollId,
content_rect: LayoutRect,
clip_rect: LayoutRect,
scroll_sensitivity: AxesScrollSensitivity,
) -> ScrollTreeNodeId {
self.compositor_info.scroll_tree.add_scroll_tree_node(
Some(parent_scroll_node_id),
SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
external_id,
content_rect,
clip_rect,
scroll_sensitivity,
offset: LayoutVector2D::zero(),
}),
)
}
let SpatialTreeNodeInfo::ReferenceFrame(ref mut info) = node.info else {
continue;
};
fn define_sticky_frame(
&mut self,
parent_scroll_node_id: &ScrollTreeNodeId,
frame_rect: LayoutRect,
margins: SideOffsets2D<Option<f32>, LayoutPixel>,
vertical_offset_bounds: StickyOffsetBounds,
horizontal_offset_bounds: StickyOffsetBounds,
) -> ScrollTreeNodeId {
self.compositor_info.scroll_tree.add_scroll_tree_node(
Some(parent_scroll_node_id),
SpatialTreeNodeInfo::Sticky(StickyNodeInfo {
frame_rect,
margins,
vertical_offset_bounds,
horizontal_offset_bounds,
}),
)
// The argument to `reference_frame_node_info_if_necessary` here is only used to calculate the
// reference frame origin. If the origin changes, we will not be doing a repaint only layout,
// and thus it's not necessary to pass a real value.
let box_fragment = box_fragment.borrow();
let Some(reference_frame_data) =
box_fragment.reference_frame_node_info_if_necessary(&PhysicalRect::zero())
else {
return;
};
info.kind = reference_frame_data.kind;
info.transform = reference_frame_data.transform;
info.transform_style = reference_frame_data.transform_style;
}
}
}
@ -889,11 +869,6 @@ impl Fragment {
}
}
struct ReferenceFrameData {
origin: crate::geom::PhysicalPoint<Au>,
transform: LayoutTransform,
kind: wr::ReferenceFrameKind,
}
struct ScrollFrameData {
scroll_tree_node_id: ScrollTreeNodeId,
scroll_frame_rect: LayoutRect,
@ -966,8 +941,8 @@ impl BoxFragment {
parent_stacking_context: &mut StackingContext,
text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) {
let reference_frame_data =
match self.reference_frame_data_if_necessary(&containing_block.rect) {
let reference_frame_node_info =
match self.reference_frame_node_info_if_necessary(&containing_block.rect) {
Some(reference_frame_data) => reference_frame_data,
None => {
return self.build_stacking_context_tree_maybe_creating_stacking_context(
@ -984,17 +959,24 @@ impl BoxFragment {
// <https://drafts.csswg.org/css-transforms/#transform-function-lists>
// > If a transform function causes the current transformation matrix of an object
// > to be non-invertible, the object and its content do not get displayed.
if !reference_frame_data.transform.is_invertible() {
if !reference_frame_node_info.transform.is_invertible() {
return;
}
let new_spatial_id = stacking_context_tree.push_reference_frame(
reference_frame_data.origin.to_webrender(),
&containing_block.scroll_node_id,
self.style.get_box().transform_style.to_webrender(),
reference_frame_data.transform,
reference_frame_data.kind,
let reference_frame_origin = Vector2D::new(
Au::from_f32_px(reference_frame_node_info.origin.x),
Au::from_f32_px(reference_frame_node_info.origin.y),
);
let new_spatial_id = stacking_context_tree
.compositor_info
.scroll_tree
.add_scroll_tree_node(
Some(&containing_block.scroll_node_id),
SpatialTreeNodeInfo::ReferenceFrame(reference_frame_node_info),
);
stacking_context_tree
.fragments_creating_spatial_nodes
.push(Some(fragment.clone()));
// WebRender reference frames establish a new coordinate system at their
// origin (the border box of the fragment). We need to ensure that any
@ -1009,10 +991,9 @@ impl BoxFragment {
self.style
.establishes_containing_block_for_all_descendants(self.base.flags)
);
let adjusted_containing_block = ContainingBlock::new(
containing_block
.rect
.translate(-reference_frame_data.origin.to_vector()),
containing_block.rect.translate(-reference_frame_origin),
new_spatial_id,
None,
containing_block.clip_id,
@ -1137,6 +1118,7 @@ impl BoxFragment {
.scroll_frame_size;
if let Some(scroll_node_id) = self.build_sticky_frame_if_necessary(
&fragment,
stacking_context_tree,
&new_scroll_node_id,
&containing_block.rect,
@ -1209,6 +1191,7 @@ impl BoxFragment {
// We want to build the scroll frame after the background and border, because
// they shouldn't scroll with the rest of the box content.
if let Some(overflow_frame_data) = self.build_overflow_frame_if_necessary(
&fragment,
stacking_context_tree,
&new_scroll_node_id,
new_clip_id,
@ -1371,6 +1354,7 @@ impl BoxFragment {
fn build_overflow_frame_if_necessary(
&self,
fragment: &Fragment,
stacking_context_tree: &mut StackingContextTree,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_id: ClipId,
@ -1463,20 +1447,28 @@ impl BoxFragment {
stacking_context_tree.compositor_info.pipeline_id,
);
let sensitivity = AxesScrollSensitivity {
let scroll_sensitivity = AxesScrollSensitivity {
x: overflow.x.into(),
y: overflow.y.into(),
};
let content_rect = self.reachable_scrollable_overflow_region().to_webrender();
let scroll_tree_node_id = stacking_context_tree.define_scroll_frame(
parent_scroll_node_id,
external_id,
content_rect,
scroll_frame_rect,
sensitivity,
);
let scroll_tree_node_id = stacking_context_tree
.compositor_info
.scroll_tree
.add_scroll_tree_node(
Some(parent_scroll_node_id),
SpatialTreeNodeInfo::Scroll(ScrollableNodeInfo {
external_id,
content_rect,
clip_rect: scroll_frame_rect,
scroll_sensitivity,
offset: LayoutVector2D::zero(),
}),
);
stacking_context_tree
.fragments_creating_spatial_nodes
.push(Some(fragment.clone()));
Some(OverflowFrameData {
clip_id,
@ -1489,6 +1481,7 @@ impl BoxFragment {
fn build_sticky_frame_if_necessary(
&self,
fragment: &Fragment,
stacking_context_tree: &mut StackingContextTree,
parent_scroll_node_id: &ScrollTreeNodeId,
containing_block_rect: &PhysicalRect<Au>,
@ -1559,22 +1552,30 @@ impl BoxFragment {
offsets.left.non_auto().map(|v| v.to_f32_px()),
);
let sticky_node_id = stacking_context_tree.define_sticky_frame(
parent_scroll_node_id,
frame_rect,
margins,
vertical_offset_bounds,
horizontal_offset_bounds,
);
let sticky_node_id = stacking_context_tree
.compositor_info
.scroll_tree
.add_scroll_tree_node(
Some(parent_scroll_node_id),
SpatialTreeNodeInfo::Sticky(StickyNodeInfo {
frame_rect,
margins,
vertical_offset_bounds,
horizontal_offset_bounds,
}),
);
stacking_context_tree
.fragments_creating_spatial_nodes
.push(Some(fragment.clone()));
Some(sticky_node_id)
}
/// Optionally returns the data for building a reference frame, without yet building it.
fn reference_frame_data_if_necessary(
fn reference_frame_node_info_if_necessary(
&self,
containing_block_rect: &PhysicalRect<Au>,
) -> Option<ReferenceFrameData> {
) -> Option<ReferenceFrameNodeInfo> {
if !self
.style
.has_effective_transform_or_perspective(self.base.flags)
@ -1612,9 +1613,10 @@ impl BoxFragment {
(None, None) => unreachable!(),
};
Some(ReferenceFrameData {
origin: border_rect.origin,
Some(ReferenceFrameNodeInfo {
origin: border_rect.origin.to_webrender(),
transform: reference_frame_transform,
transform_style: self.style.get_box().transform_style.to_webrender(),
kind: reference_frame_kind,
})
}

View file

@ -846,10 +846,12 @@ impl LayoutThread {
let Some(fragment_tree) = &*self.fragment_tree.borrow() else {
return;
};
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) &&
self.stacking_context_tree.borrow().is_some()
{
return;
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) {
if let Some(stacking_context_tree) = &mut *self.stacking_context_tree.borrow_mut() {
stacking_context_tree.repair_scroll_tree_for_repaint_only_layout();
return;
}
}
let viewport_size = self.stylist.device().au_viewport_size();