From d2a7fee8c55d192e3589aef4a6f4cdfd8719b171 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 14:50:36 +0200 Subject: [PATCH 01/14] LibWeb: Carry fragments in links behind a shared pointer The formatting-context run cache being built next re-deposits a completed run's fragment tree on every hit, so links must be able to hand out the same position-independent subtree more than once. Flip FragmentLink::fragment from Box to Rc and derive Clone on the link, the unplaced root, and every payload a cached tree carries: line data, the owned grid and flex payloads, and the propagated escape structs. Completed fragments are immutable after their snapshot (the SVG path cell is the one interior-mutable field), so sharing the allocation changes no behavior. --- .../LibWeb/Rust/src/layout/formatting_context.rs | 2 ++ Libraries/LibWeb/Rust/src/layout/fragment_tree.rs | 13 +++++++++---- .../Rust/src/layout/grid_formatting_context.rs | 6 ++++++ Libraries/LibWeb/Rust/src/layout/used_values.rs | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 28a0819feb078..b3a724ed26528 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -906,6 +906,7 @@ pub struct FfiFlexLayoutData { pub line_count: usize, } +#[derive(Clone)] pub(crate) struct OwnedFlexLayoutLine { pub(crate) growth_state: FfiFlexLayoutGrowthState, pub(crate) cross_start: CssPixels, @@ -913,6 +914,7 @@ pub(crate) struct OwnedFlexLayoutLine { pub(crate) items: Vec, } +#[derive(Clone)] pub(crate) struct OwnedFlexLayoutData { pub(crate) align_content: u8, pub(crate) align_items: u8, diff --git a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs index ca4ce8a0cba42..a0d2c5fa7c32f 100644 --- a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs +++ b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs @@ -32,8 +32,9 @@ pub(crate) struct Fragment { pub(crate) children: Vec, } +#[derive(Clone)] pub(crate) struct FragmentLink { - pub(crate) fragment: Box, + pub(crate) fragment: std::rc::Rc, pub(crate) committed_offset: FfiCssPixelPoint, pub(crate) inset_left: CssPixels, pub(crate) inset_right: CssPixels, @@ -43,6 +44,7 @@ pub(crate) struct FragmentLink { pub(crate) abspos_layout_inputs: Option, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct AnchorCandidate { pub(crate) node: crate::layout::node_data::NodeSlotId, pub(crate) border_box_rect: PhysicalRect, @@ -51,6 +53,7 @@ pub(crate) struct AnchorCandidate { /// The padding-box rect of an inline box that acts as an abspos containing /// block, spanning its first and last content lines. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct InlineContainingBlockRect { pub(crate) inline_box: crate::layout::node_data::NodeSlotId, pub(crate) rect: PhysicalRect, @@ -62,6 +65,7 @@ pub(crate) struct InlineContainingBlockRect { /// descendants). The rect is relative to the containing block's own content /// origin, so the contribution never rebases; it travels as-is to whichever /// run drains the child and is joined by child-box identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct AbsposContainingBlockInfoContribution { pub(crate) child_box: crate::layout::node_data::NodeSlotId, pub(crate) info: AbsposContainingBlockInfo, @@ -151,7 +155,7 @@ fn snapshot_fragment( node: crate::layout::node_data::NodeSlotId, children: Vec, used: &UsedValues, -) -> Box { +) -> std::rc::Rc { let line_data = used.line_data.get().map(|cell| Box::new(cell.take())); let rare_payloads = used.rare_data.get().map(|cell| { let mut rare = cell.borrow_mut(); @@ -176,7 +180,7 @@ fn snapshot_fragment( svg_viewport_size, computed_svg_path, ) = rare_payloads.unwrap_or_default(); - Box::new(Fragment { + std::rc::Rc::new(Fragment { node, content_inline_size: used.content_inline_size.get(), content_block_size: used.content_block_size.get(), @@ -236,7 +240,7 @@ impl PlacementData { } } -fn link_fragment(fragment: Box, placement: PlacementData) -> FragmentLink { +fn link_fragment(fragment: std::rc::Rc, placement: PlacementData) -> FragmentLink { FragmentLink { fragment, committed_offset: placement.committed_offset, @@ -249,6 +253,7 @@ fn link_fragment(fragment: Box, placement: PlacementData) -> FragmentL } } +#[derive(Clone)] pub(crate) struct UnplacedRootFragment { pub(crate) node: crate::layout::node_data::NodeSlotId, pub(crate) scoped_descendants: Vec, diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index 0d9bb1cd34417..273755fa0c969 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -335,6 +335,7 @@ pub struct FfiUsedGridTrackList { pub track_count: usize, } +#[derive(Clone)] pub(crate) struct OwnedGridLayoutLine { pub(crate) names: Vec, pub(crate) start: crate::layout::CssPixels, @@ -358,17 +359,20 @@ impl OwnedGridLayoutLine { } } +#[derive(Clone)] pub(crate) struct OwnedGridLayoutDimension { pub(crate) lines: Vec, pub(crate) tracks: Vec, } +#[derive(Clone)] pub(crate) struct OwnedGridLayoutFragment { pub(crate) areas: Vec, pub(crate) columns: OwnedGridLayoutDimension, pub(crate) rows: OwnedGridLayoutDimension, } +#[derive(Clone)] pub(crate) struct OwnedGridLayoutData { pub(crate) direction: u8, pub(crate) writing_mode: u8, @@ -435,6 +439,7 @@ impl OwnedGridLayoutData { } } +#[derive(Clone)] pub(crate) struct OwnedUsedGridTrackList { pub(crate) is_subgrid: bool, pub(crate) lines: Vec>, @@ -453,6 +458,7 @@ impl OwnedUsedGridTrackList { } } +#[derive(Clone)] pub(crate) struct OwnedUsedGridTracks { pub(crate) columns: OwnedUsedGridTrackList, pub(crate) rows: OwnedUsedGridTrackList, diff --git a/Libraries/LibWeb/Rust/src/layout/used_values.rs b/Libraries/LibWeb/Rust/src/layout/used_values.rs index 15056c9896ed2..6d12496554d57 100644 --- a/Libraries/LibWeb/Rust/src/layout/used_values.rs +++ b/Libraries/LibWeb/Rust/src/layout/used_values.rs @@ -114,7 +114,7 @@ impl SealableCell { } } -#[derive(Default)] +#[derive(Clone, Default)] pub(crate) struct LineData { pub(crate) line_boxes: Vec, pub(crate) inline_box_pieces: Vec, From 38b2949a34110a7df6b92367dd2e72829e7080dc Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 18:27:36 +0200 Subject: [PATCH 02/14] LibWeb: Carry line data and owned layout payloads behind shared pointers A run root's line data and its grid and flex payloads travel by value from the record through the run outcome into the fragment, so the run cache deep-copied every glyph vector and track list once at store time and again on every hit, and text- or grid-heavy pages paid line-data allocation twice per pass. Put LineData and the three payload types behind Rc end to end. All line data mutation happens inside the owning run, so the record cell hands out mutable access through Rc::make_mut while the count is one and nothing ever clones; the payloads are written once at run end. Store and hit become refcount bumps, commit keeps reading borrows, and the payload types no longer need Clone at all. --- .../src/layout/flex_formatting_context.rs | 2 +- .../Rust/src/layout/formatting_context.rs | 8 ++++---- .../LibWeb/Rust/src/layout/fragment_tree.rs | 10 +++++----- .../src/layout/grid_formatting_context.rs | 20 +++++++++---------- .../src/layout/inline_formatting_context.rs | 4 ++-- Libraries/LibWeb/Rust/src/layout/line_box.rs | 4 ++-- .../Rust/src/layout/line_box_fragment.rs | 6 +++--- .../LibWeb/Rust/src/layout/used_values.rs | 16 +++++++-------- 8 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs index 1d5d47bf6821b..8f49b38e12155 100644 --- a/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/flex_formatting_context.rs @@ -2555,7 +2555,7 @@ impl<'pass> FlexFormattingContext<'pass> { as u8, lines, }; - self.container_used().rare_data_mut().flex_layout_data = Some(data); + self.container_used().rare_data_mut().flex_layout_data = Some(std::rc::Rc::new(data)); } // https://drafts.csswg.org/css-sizing-4/#aspect-ratio-automatic diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index b3a724ed26528..199fb99c13f18 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -788,7 +788,7 @@ pub(crate) struct ChildLayoutResult { pub(crate) struct RunRootOutcome { cells: UsedValuesCellState, own_metrics_sealed: bool, - line_data: Option, + line_data: Option>, rare: Option, } @@ -866,7 +866,7 @@ pub enum FfiFlexLayoutGrowthState { Shrinking, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[repr(C)] pub struct FfiFlexLayoutItem { pub node: *mut c_void, @@ -906,7 +906,7 @@ pub struct FfiFlexLayoutData { pub line_count: usize, } -#[derive(Clone)] +#[derive(PartialEq)] pub(crate) struct OwnedFlexLayoutLine { pub(crate) growth_state: FfiFlexLayoutGrowthState, pub(crate) cross_start: CssPixels, @@ -914,7 +914,7 @@ pub(crate) struct OwnedFlexLayoutLine { pub(crate) items: Vec, } -#[derive(Clone)] +#[derive(PartialEq)] pub(crate) struct OwnedFlexLayoutData { pub(crate) align_content: u8, pub(crate) align_items: u8, diff --git a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs index a0d2c5fa7c32f..a8b2aced1c9b6 100644 --- a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs +++ b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs @@ -22,10 +22,10 @@ pub(crate) struct Fragment { pub(crate) padding_bottom: CssPixels, pub(crate) table_cell_coordinates: Option, pub(crate) override_borders_data: Option, - pub(crate) line_data: Option>, - pub(crate) grid_layout_data: Option, - pub(crate) flex_layout_data: Option, - pub(crate) used_grid_tracks: Option, + pub(crate) line_data: Option>, + pub(crate) grid_layout_data: Option>, + pub(crate) flex_layout_data: Option>, + pub(crate) used_grid_tracks: Option>, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, pub(crate) computed_svg_path: Cell>, @@ -156,7 +156,7 @@ fn snapshot_fragment( children: Vec, used: &UsedValues, ) -> std::rc::Rc { - let line_data = used.line_data.get().map(|cell| Box::new(cell.take())); + let line_data = used.line_data.get().map(std::cell::RefCell::take); let rare_payloads = used.rare_data.get().map(|cell| { let mut rare = cell.borrow_mut(); ( diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index 273755fa0c969..885508c63c952 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -270,7 +270,7 @@ pub struct FfiGridLayoutLine { pub negative_number: i32, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct FfiGridLayoutTrack { pub start: crate::layout::CssPixels, @@ -288,7 +288,7 @@ pub struct FfiGridLayoutDimension { pub track_count: usize, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct FfiGridLayoutArea { pub name: usize, @@ -335,7 +335,7 @@ pub struct FfiUsedGridTrackList { pub track_count: usize, } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutLine { pub(crate) names: Vec, pub(crate) start: crate::layout::CssPixels, @@ -359,20 +359,20 @@ impl OwnedGridLayoutLine { } } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutDimension { pub(crate) lines: Vec, pub(crate) tracks: Vec, } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutFragment { pub(crate) areas: Vec, pub(crate) columns: OwnedGridLayoutDimension, pub(crate) rows: OwnedGridLayoutDimension, } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedGridLayoutData { pub(crate) direction: u8, pub(crate) writing_mode: u8, @@ -439,7 +439,7 @@ impl OwnedGridLayoutData { } } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedUsedGridTrackList { pub(crate) is_subgrid: bool, pub(crate) lines: Vec>, @@ -458,7 +458,7 @@ impl OwnedUsedGridTrackList { } } -#[derive(Clone)] +#[derive(PartialEq, Eq)] pub(crate) struct OwnedUsedGridTracks { pub(crate) columns: OwnedUsedGridTrackList, pub(crate) rows: OwnedUsedGridTrackList, @@ -3304,7 +3304,7 @@ impl GridFormattingContext { }; self.container_used() .rare_data_mut() - .used_grid_tracks = Some(tracks); + .used_grid_tracks = Some(std::rc::Rc::new(tracks)); } fn save_devtools_data(&self, grid_style: &GridValues) { @@ -3411,7 +3411,7 @@ impl GridFormattingContext { }; self.container_used() .rare_data_mut() - .grid_layout_data = Some(data); + .grid_layout_data = Some(std::rc::Rc::new(data)); } pub(crate) fn run(&mut self, run: &FormattingContextRun, input: LayoutInput) { diff --git a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs index 20f6aef7c47ec..0eb2cd8971ed5 100644 --- a/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/inline_formatting_context.rs @@ -792,11 +792,11 @@ impl<'context> InlineFormattingContext<'context> { } pub(crate) fn line_data(&self) -> Ref<'_, LineData> { - self.containing_used_values.line_data_cell().borrow() + Ref::map(self.containing_used_values.line_data_cell().borrow(), |shared| &**shared) } pub(crate) fn line_data_mut(&self) -> RefMut<'_, LineData> { - self.containing_used_values.line_data_cell().borrow_mut() + RefMut::map(self.containing_used_values.line_data_cell().borrow_mut(), std::rc::Rc::make_mut) } pub(crate) fn containing_used(&self) -> std::rc::Rc { diff --git a/Libraries/LibWeb/Rust/src/layout/line_box.rs b/Libraries/LibWeb/Rust/src/layout/line_box.rs index 8bb25ad0efc11..5c3772a783a83 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_box.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_box.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StaticPositionMarker { pub(crate) box_: Node, pub(crate) inline_offset: CssPixels, @@ -19,7 +19,7 @@ impl StaticPositionMarker { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct LineBoxData { pub(crate) fragments: Vec, pub(crate) static_position_markers: Vec, diff --git a/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs b/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs index 44b70447b29cf..9fb063f1b8cca 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_box_fragment.rs @@ -14,7 +14,7 @@ pub(crate) fn is_ascii_space(code_unit: u16) -> bool { matches!(code_unit, 0x09..=0x0d | 0x20) } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct GlyphData { pub(crate) glyphs: Vec, pub(crate) font: *const c_void, @@ -25,13 +25,13 @@ pub(crate) struct GlyphData { // The advance of a run's trailing whitespace, recorded at shaping time so that trimming it subtracts exactly what // shaping added. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub(crate) struct TrailingWhitespace { pub(crate) length_in_code_units: usize, pub(crate) inline_size: CssPixels, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct LineBoxFragmentData { pub(crate) layout_node: Node, pub(crate) style_source: Node, diff --git a/Libraries/LibWeb/Rust/src/layout/used_values.rs b/Libraries/LibWeb/Rust/src/layout/used_values.rs index 6d12496554d57..9aaf2435313d4 100644 --- a/Libraries/LibWeb/Rust/src/layout/used_values.rs +++ b/Libraries/LibWeb/Rust/src/layout/used_values.rs @@ -114,7 +114,7 @@ impl SealableCell { } } -#[derive(Clone, Default)] +#[derive(Clone, Default, PartialEq)] pub(crate) struct LineData { pub(crate) line_boxes: Vec, pub(crate) inline_box_pieces: Vec, @@ -126,9 +126,9 @@ pub(crate) struct UsedValuesRareData { pub(crate) computed_svg_path: Option, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, - pub(crate) grid_layout_data: Option, - pub(crate) flex_layout_data: Option, - pub(crate) used_grid_tracks: Option, + pub(crate) grid_layout_data: Option>, + pub(crate) flex_layout_data: Option>, + pub(crate) used_grid_tracks: Option>, pub(crate) override_borders_data: Option, pub(crate) abspos_layout_inputs: Option, } @@ -227,7 +227,7 @@ pub(crate) struct UsedValues { pub last_baseline: Cell, - pub(crate) line_data: LazyRefCell, + pub(crate) line_data: LazyRefCell>, pub(crate) rare_data: LazyRefCell, } @@ -276,11 +276,11 @@ impl UsedValues { } pub(crate) fn line_data_ref(&self) -> Option> { - self.line_data.get().map(RefCell::borrow) + self.line_data.get().map(|cell| Ref::map(cell.borrow(), |shared| &**shared)) } - pub(crate) fn line_data_cell(&self) -> &RefCell { - self.line_data.get_or_init(LineData::default) + pub(crate) fn line_data_cell(&self) -> &RefCell> { + self.line_data.get_or_init(std::rc::Rc::default) } pub(crate) fn content_baselines_from_cells(&self) -> crate::layout::DerivedBaselines { From 5fc8e44d00bf2af8d2a579bf0775508d32219384 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 14:56:45 +0200 Subject: [PATCH 03/14] LibWeb: Detect anchor-function insets with a node flag The partial relayout boundary check reconstructed inset style values on every query to look for anchor functions, and it missed bare anchor() insets entirely: those are not stored in the inset length box, they live in the per-side anchor inset handles kept next to it, so the is_calculated early-out returned false and a bare-anchor consumer could qualify as a boundary and replay stale anchor positions. Move the predicate onto ComputedValues, where the anchor inset handles are checked before the calculated-inset fast path, and stamp the result into a node flag whenever computed values are set. The boundary check reads the flag, and the formatting-context run cache added later will gate its probes on the same flag. The standalone bridge predicate is deleted. --- Libraries/LibWeb/CSS/ComputedProperties.cpp | 32 +++++++++++++++ Libraries/LibWeb/CSS/ComputedValues.h | 40 +++++++++++-------- Libraries/LibWeb/Layout/Box.cpp | 2 +- Libraries/LibWeb/Layout/LayoutRustBridge.cpp | 35 ---------------- Libraries/LibWeb/Layout/LayoutRustBridge.h | 1 - Libraries/LibWeb/Layout/Node.cpp | 2 + Libraries/LibWeb/Layout/Node.h | 1 + Libraries/LibWeb/Rust/src/layout/node_data.rs | 1 + 8 files changed, 61 insertions(+), 53 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index c5f2b3062fc9b..70a8735ccb8f4 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -131,6 +131,38 @@ RefPtr ComputedValues::background_color_style_value() const static_assert(to_underlying(PseudoElement::KnownPseudoElementCount) <= sizeof(u64) * 8); +static bool style_value_contains_anchor_function(StyleValue const& value) +{ + if (value.is_anchor()) + return true; + if (value.is_calculated()) + return value.as_calculated().contains_anchor_function(); + return false; +} + +bool ComputedValues::inset_properties_contain_anchor_functions() const +{ + // A bare anchor function is not stored in the inset length box at all: it lives in the + // per-side anchor inset handles kept next to it. + if (has_anchor_inset(PropertyID::Top) || has_anchor_inset(PropertyID::Right) + || has_anchor_inset(PropertyID::Bottom) || has_anchor_inset(PropertyID::Left)) + return true; + // Anchor functions inside expressions survive to used-value time as calculated values, so + // when no inset is calculated (the common case), skip reconstructing the style values. + auto const& inset_box = inset(); + if (!inset_box.top().is_calculated() && !inset_box.right().is_calculated() && !inset_box.bottom().is_calculated() && !inset_box.left().is_calculated()) + return false; + auto top = computed_style_value(PropertyID::Top); + auto right = computed_style_value(PropertyID::Right); + auto bottom = computed_style_value(PropertyID::Bottom); + auto left = computed_style_value(PropertyID::Left); + VERIFY(top && right && bottom && left); + return style_value_contains_anchor_function(*top) + || style_value_contains_anchor_function(*right) + || style_value_contains_anchor_function(*bottom) + || style_value_contains_anchor_function(*left); +} + RefPtr ComputedValues::computed_style_value(PropertyID property_id, WithAnimationsApplied with_animations_applied) const { if (with_animations_applied == WithAnimationsApplied::No && m_base_values) diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 697883c3d1f78..4dc8190887288 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -1371,25 +1371,17 @@ class WEB_API ComputedValues final : public RefCounted { ShapeRendering shape_rendering() const { return static_cast(m_noninherited.svg_reset->shape_rendering); } LengthBox inset() const { return length_box(m_noninherited.surround->inset); } + bool has_anchor_inset(PropertyID property_id) const + { + auto const* handle = anchor_inset_handle(property_id); + return handle && handle->pointer != nullptr; + } + bool inset_properties_contain_anchor_functions() const; RefPtr anchor_inset(PropertyID property_id) const { - ComputedValuesFFI::ComputedStyleValueHandle const* handle = nullptr; - switch (property_id) { - case PropertyID::Top: - handle = &m_noninherited.surround->top_anchor_inset; - break; - case PropertyID::Right: - handle = &m_noninherited.surround->right_anchor_inset; - break; - case PropertyID::Bottom: - handle = &m_noninherited.surround->bottom_anchor_inset; - break; - case PropertyID::Left: - handle = &m_noninherited.surround->left_anchor_inset; - break; - default: + auto const* handle = anchor_inset_handle(property_id); + if (!handle) return {}; - } static_assert(sizeof(RustStyleValueHandle) == sizeof(*handle)); return style_value_from_handle(property_id, reinterpret_cast(*handle)); } @@ -1530,6 +1522,22 @@ class WEB_API ComputedValues final : public RefCounted { RefPtr style_value_from_handle(PropertyID, RustStyleValueHandle const&) const; + ComputedValuesFFI::ComputedStyleValueHandle const* anchor_inset_handle(PropertyID property_id) const + { + switch (property_id) { + case PropertyID::Top: + return &m_noninherited.surround->top_anchor_inset; + case PropertyID::Right: + return &m_noninherited.surround->right_anchor_inset; + case PropertyID::Bottom: + return &m_noninherited.surround->bottom_anchor_inset; + case PropertyID::Left: + return &m_noninherited.surround->left_anchor_inset; + default: + return nullptr; + } + } + static LengthPercentageOrAuto length_percentage_or_auto(ComputedValuesFFI::ComputedLengthPercentageOrAuto const& value) { if (value.is_auto) diff --git a/Libraries/LibWeb/Layout/Box.cpp b/Libraries/LibWeb/Layout/Box.cpp index d0a3c806a1e2a..39df7dbfa43a5 100644 --- a/Libraries/LibWeb/Layout/Box.cpp +++ b/Libraries/LibWeb/Layout/Box.cpp @@ -59,7 +59,7 @@ bool Box::is_partial_relayout_boundary(RequireExistingPaintable require_existing // Only a full layout pass resolves anchor() functions in the inset properties to plain // values; a replay from saved inputs cannot. - if (box_inset_properties_contain_anchor_functions(*this)) + if (insets_use_anchor_functions()) return false; // NOTE: Content-dependent sizing (shrink-to-fit, intrinsic constraints, aspect-ratio) does diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index c3137b9faafab..0d8b34c5a89dc 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -1014,41 +1014,6 @@ static Optional abstract_element_for_abspos_box(Box const& return {}; } -static bool style_value_contains_anchor(CSS::StyleValue const& value) -{ - if (value.is_anchor()) - return true; - if (value.is_calculated()) - return value.as_calculated().contains_anchor_function(); - return false; -} - -bool box_inset_properties_contain_anchor_functions(Box const& box) -{ - auto abstract_element = abstract_element_for_abspos_box(box); - if (!abstract_element.has_value()) - return false; - - auto const* computed = abstract_element->computed_values(); - if (!computed) - return false; - // Anchor functions in insets only survive to used-value time inside calculated values, so - // when no inset is calculated (the common case), skip reconstructing the style values. - auto const& inset = computed->inset(); - if (!inset.top().is_calculated() && !inset.right().is_calculated() && !inset.bottom().is_calculated() && !inset.left().is_calculated()) - return false; - - auto top = computed->computed_style_value(CSS::PropertyID::Top); - auto right = computed->computed_style_value(CSS::PropertyID::Right); - auto bottom = computed->computed_style_value(CSS::PropertyID::Bottom); - auto left = computed->computed_style_value(CSS::PropertyID::Left); - VERIFY(top && right && bottom && left); - return style_value_contains_anchor(*top) - || style_value_contains_anchor(*right) - || style_value_contains_anchor(*bottom) - || style_value_contains_anchor(*left); -} - bool can_replay_saved_abspos_layout_inputs_after_style_change(Box const& box) { if (!box.containing_block()) diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.h b/Libraries/LibWeb/Layout/LayoutRustBridge.h index c324a95008a6f..6beadc3e99686 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.h +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.h @@ -50,7 +50,6 @@ class LayoutRustBridge { [[nodiscard]] Optional formatting_context_type_created_by_box(Box const&); [[nodiscard]] StringView formatting_context_type_name(RustFFI::FfiFormattingContextType); -[[nodiscard]] bool box_inset_properties_contain_anchor_functions(Box const&); [[nodiscard]] bool can_replay_saved_abspos_layout_inputs_after_style_change(Box const&); // True while a synchronous Rust layout pass (including its commit) is on the diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index e6caf2b809c22..387b4e81d7467 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -669,6 +669,7 @@ NodeWithStyle::NodeWithStyle(DOM::Document& document, GC::Ptr node, N set_flag(RustFFI::NodeFlag::HasStyle, true); set_flag(RustFFI::NodeFlag::IsBody, node && node == GC::Ptr { document.body() }); set_flag(RustFFI::NodeFlag::HasAnchorNames, !m_computed_values->anchor_names().is_empty()); + set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, m_computed_values->inset_properties_contain_anchor_functions()); publish_style_container_to_node_data(); synchronize_table_span_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); @@ -1010,6 +1011,7 @@ void NodeWithStyle::set_computed_values(NonnullRefPtr VERIFY(!layout_pass_currently_running()); m_computed_values = move(computed_values); set_flag(RustFFI::NodeFlag::HasAnchorNames, !m_computed_values->anchor_names().is_empty()); + set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, m_computed_values->inset_properties_contain_anchor_functions()); publish_style_container_to_node_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index a3456958675d1..4701b6c92d1b0 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -105,6 +105,7 @@ class WEB_API Node NodeArena& node_arena() const { return *m_arena; } bool is_anonymous() const { return has_flag(RustFFI::NodeFlag::Anonymous); } + bool insets_use_anchor_functions() const { return has_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions); } DOM::Node const* dom_node() const; DOM::Node* dom_node(); diff --git a/Libraries/LibWeb/Rust/src/layout/node_data.rs b/Libraries/LibWeb/Rust/src/layout/node_data.rs index d803ba0586664..cfc0b99ba7e46 100644 --- a/Libraries/LibWeb/Rust/src/layout/node_data.rs +++ b/Libraries/LibWeb/Rust/src/layout/node_data.rs @@ -159,6 +159,7 @@ pub enum NodeFlag { ProducesLineBoxFragmentWhenEmpty = 1 << 22, ListMarkerIsInside = 1 << 23, HasAnchorNames = 1 << 24, + InsetsUseAnchorFunctions = 1 << 25, } #[repr(C)] From 6a220d0096c699f89a3eb1fa8913ed431ad74669 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 14:59:35 +0200 Subject: [PATCH 04/14] LibWeb: Emit SVG paths from shared fragment handles A computed SVG path was move-only: commit took it out of the fragment's cell and the C++ sink moved the contents out of the heap allocation, so a fragment tree containing one SVG shape could only be emitted once. The formatting-context run cache needs to re-emit cached trees, and under the old contract a single svg shape would have disqualified every ancestor run's entry. Carry the path behind Rc in the record's rare data and the fragment, emit it as a borrow, and copy the contents on the C++ side of the sink. Rust keeps sole ownership of the allocation and frees it when the last holder drops, so re-committing the same tree now emits the same path every time. Each allocation carries a process-unique, never-reused identity, and the paintable keeps its committed path across relayout, swapping it only when the emitted identity changes, so an unchanged shape costs no deep copy on recommit. Commit asserts that every committed path-like fragment carries a path, which is what makes keeping the previous one sound. The allocation also compares by content over a Skia path equality export, behind the identity as a fast path. --- Libraries/LibGfx/Path.cpp | 9 +++++++++ Libraries/LibGfx/Rust/src/path.rs | 20 +++++++++++++++++++ Libraries/LibWeb/Layout/LayoutRustBridge.cpp | 10 ++++++---- .../LibWeb/Painting/SVGPathPaintable.cpp | 6 ------ Libraries/LibWeb/Painting/SVGPathPaintable.h | 14 +++++++++---- Libraries/LibWeb/Rust/src/layout/commit.rs | 17 +++++++++++++--- .../LibWeb/Rust/src/layout/fragment_tree.rs | 4 ++-- .../Rust/src/layout/svg_formatting_context.rs | 2 +- .../LibWeb/Rust/src/layout/used_values.rs | 2 +- 9 files changed, 63 insertions(+), 21 deletions(-) diff --git a/Libraries/LibGfx/Path.cpp b/Libraries/LibGfx/Path.cpp index a154df40ec9fb..5c1892d985f14 100644 --- a/Libraries/LibGfx/Path.cpp +++ b/Libraries/LibGfx/Path.cpp @@ -8,9 +8,11 @@ #include #include #include +#include extern "C" { void ladybird_gfx_path_destroy(void*); +bool ladybird_gfx_path_equals(void const*, void const*); } namespace Gfx { @@ -52,3 +54,10 @@ extern "C" void ladybird_gfx_path_destroy(void* path) { delete static_cast(path); } + +extern "C" bool ladybird_gfx_path_equals(void const* a, void const* b) +{ + auto const& path_a = *static_cast(a); + auto const& path_b = *static_cast(b); + return static_cast(path_a.impl()).sk_path() == static_cast(path_b.impl()).sk_path(); +} diff --git a/Libraries/LibGfx/Rust/src/path.rs b/Libraries/LibGfx/Rust/src/path.rs index f667b1fdafebe..6a92174dc3e08 100644 --- a/Libraries/LibGfx/Rust/src/path.rs +++ b/Libraries/LibGfx/Rust/src/path.rs @@ -9,11 +9,13 @@ use std::ptr::NonNull; unsafe extern "C" { fn ladybird_gfx_path_destroy(path: *mut c_void); + fn ladybird_gfx_path_equals(a: *const c_void, b: *const c_void) -> bool; } /// The sole owner of a heap-allocated `Gfx::Path`, destroying it on drop. pub struct OwnedPath { raw: NonNull, + identity: u64, } impl OwnedPath { @@ -25,8 +27,10 @@ impl OwnedPath { /// `Gfx::Path` is owned or destroyed. #[inline] pub unsafe fn adopt(raw: *mut c_void) -> Self { + static NEXT_IDENTITY: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); Self { raw: NonNull::new(raw).expect("Gfx::Path pointer must not be null"), + identity: NEXT_IDENTITY.fetch_add(1, std::sync::atomic::Ordering::Relaxed), } } @@ -35,6 +39,22 @@ impl OwnedPath { pub fn as_raw(&self) -> *mut c_void { self.raw.as_ptr() } + + /// A process-unique, never-reused identity for this path allocation, so + /// consumers holding a copied snapshot can recognize an unchanged path + /// without comparing contents. + #[inline] + pub fn identity(&self) -> u64 { + self.identity + } +} + +impl PartialEq for OwnedPath { + fn eq(&self, other: &Self) -> bool { + self.identity == other.identity + // SAFETY: Both sides own live heap-allocated paths for the duration of the call. + || unsafe { ladybird_gfx_path_equals(self.raw.as_ptr(), other.raw.as_ptr()) } + } } impl Drop for OwnedPath { diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 0d8b34c5a89dc..8bff24a1cd4e3 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -942,13 +942,15 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() CSSPixels::from_raw(viewport_size.height), }); } }, - .set_computed_svg_path = [](void*, void* paintable_pointer, void* path_pointer) { + .set_computed_svg_path = [](void*, void* paintable_pointer, void* path_pointer, u64 path_identity) { VERIFY(path_pointer); auto& paintable = *static_cast(paintable_pointer); - // The path stays owned by the Rust layout state; only its contents move out. - auto* path = static_cast(path_pointer); + // The path stays owned by the Rust fragment tree, which may emit it again on a + // later commit; the identity is process-unique per path allocation, so a match + // means the preserved copy is already this exact path and the copy can be skipped. + auto const* path = static_cast(path_pointer); if (auto* svg_path_paintable = as_if(paintable)) - svg_path_paintable->set_computed_path(move(*path)); }, + svg_path_paintable->set_computed_path_if_identity_changed(*path, path_identity); }, .set_grid_layout_data = [](void*, void* paintable_pointer, RustFFI::FfiGridLayoutData const* data) { VERIFY(data); static_cast(paintable_pointer)->set_grid_layout_data(build_grid_layout_data(*data)); }, diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp index e3d1055f55538..c50762925d4fa 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.cpp @@ -23,12 +23,6 @@ SVGPathPaintable::SVGPathPaintable(Layout::SVGGraphicsBox const& layout_box) { } -void SVGPathPaintable::reset_for_relayout() -{ - SVGGraphicsPaintable::reset_for_relayout(); - m_computed_path.clear(); -} - Optional SVGPathPaintable::clip_path_geometry_bounds(Gfx::AffineTransform const& additional_transform) const { if (!contributes_to_clip_path() || !computed_path().has_value()) diff --git a/Libraries/LibWeb/Painting/SVGPathPaintable.h b/Libraries/LibWeb/Painting/SVGPathPaintable.h index 5fb850469bbb6..86d8a950d0f28 100644 --- a/Libraries/LibWeb/Painting/SVGPathPaintable.h +++ b/Libraries/LibWeb/Painting/SVGPathPaintable.h @@ -25,19 +25,25 @@ class WEB_API SVGPathPaintable final : public SVGGraphicsPaintable { SVG::SVGGraphicsElement const& dom_node() const { return as(*Paintable::dom_node()); } - void set_computed_path(Gfx::Path path) + // The identity is process-unique per layout-side path allocation and never + // reused, so a match proves the already-held path is byte-identical and the + // deep copy can be skipped. Every commit of a path-like fragment emits a + // path (commit-side asserted), so the held path is never stale. + void set_computed_path_if_identity_changed(Gfx::Path const& path, u64 path_identity) { - m_computed_path = move(path); + if (path_identity != 0 && path_identity == m_committed_path_identity && m_computed_path.has_value()) + return; + m_computed_path = path; + m_committed_path_identity = path_identity; } Optional const& computed_path() const { return m_computed_path; } - virtual void reset_for_relayout() override; - protected: SVGPathPaintable(Layout::SVGGraphicsBox const&); Optional m_computed_path = {}; + u64 m_committed_path_identity { 0 }; private: virtual bool is_svg_path_paintable() const final { return true; } diff --git a/Libraries/LibWeb/Rust/src/layout/commit.rs b/Libraries/LibWeb/Rust/src/layout/commit.rs index f658439b055e0..50d8b7c490e5c 100644 --- a/Libraries/LibWeb/Rust/src/layout/commit.rs +++ b/Libraries/LibWeb/Rust/src/layout/commit.rs @@ -95,7 +95,7 @@ pub struct FfiCommitSink { pub finish_line_data: unsafe extern "C" fn(*mut c_void), pub set_computed_svg_transforms: unsafe extern "C" fn(*mut c_void, *mut c_void, crate::layout::FfiSvgComputedTransforms), pub set_svg_viewport_size: unsafe extern "C" fn(*mut c_void, *mut c_void, crate::layout::FfiCssPixelSize), - pub set_computed_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void), + pub set_computed_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, u64), pub set_grid_layout_data: unsafe extern "C" fn(*mut c_void, *mut c_void, *const FfiGridLayoutData), pub set_flex_layout_data: unsafe extern "C" fn(*mut c_void, *mut c_void, *const FfiFlexLayoutData), pub set_used_grid_tracks: @@ -195,8 +195,19 @@ fn commit_subtree( if let Some(viewport_size) = fragment.svg_viewport_size { (sink.set_svg_viewport_size)(sink.context, paintable, viewport_size); } - if let Some(path) = fragment.computed_svg_path.take() { - (sink.set_computed_svg_path)(sink.context, paintable, path.as_raw()); + // The paintable keeps its committed path across relayout and only swaps it on an + // identity change, which is sound only while every committed path-like fragment + // carries a path. + debug_assert!( + fragment.computed_svg_path.is_some() + || !matches!( + callbacks.node_data(node).kind, + NodeKind::SVGGeometryBox | NodeKind::SVGTextBox | NodeKind::SVGTextPathBox + ), + "committed path-like fragment carries no computed SVG path" + ); + if let Some(path) = &fragment.computed_svg_path { + (sink.set_computed_svg_path)(sink.context, paintable, path.as_raw(), path.identity()); } } if let Some(data) = &fragment.grid_layout_data { diff --git a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs index a8b2aced1c9b6..1b7b9ef9e91b6 100644 --- a/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs +++ b/Libraries/LibWeb/Rust/src/layout/fragment_tree.rs @@ -28,7 +28,7 @@ pub(crate) struct Fragment { pub(crate) used_grid_tracks: Option>, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, - pub(crate) computed_svg_path: Cell>, + pub(crate) computed_svg_path: Option>, pub(crate) children: Vec, } @@ -204,7 +204,7 @@ fn snapshot_fragment( used_grid_tracks, computed_svg_transforms, svg_viewport_size, - computed_svg_path: Cell::new(computed_svg_path), + computed_svg_path, children, }) } diff --git a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs index 4a21d809e7f9f..2f2ce667f8807 100644 --- a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs @@ -967,7 +967,7 @@ impl SvgFormattingContext { let used = &used_pointer; used.set_content_inline_size(transformed_bounding_box.width); used.set_content_block_size(transformed_bounding_box.height); - self.used_values(graphics_box).rare_data_mut().computed_svg_path = Some(path); + self.used_values(graphics_box).rare_data_mut().computed_svg_path = Some(std::rc::Rc::new(path)); self.place_child(graphics_box, transformed_bounding_box.x, transformed_bounding_box.y); used.has_definite_inline_size.set(true); used.has_definite_block_size.set(true); diff --git a/Libraries/LibWeb/Rust/src/layout/used_values.rs b/Libraries/LibWeb/Rust/src/layout/used_values.rs index 9aaf2435313d4..4fe4fc28e37b9 100644 --- a/Libraries/LibWeb/Rust/src/layout/used_values.rs +++ b/Libraries/LibWeb/Rust/src/layout/used_values.rs @@ -123,7 +123,7 @@ pub(crate) struct LineData { #[derive(Default)] pub(crate) struct UsedValuesRareData { pub(crate) table_cell_coordinates: Option, - pub(crate) computed_svg_path: Option, + pub(crate) computed_svg_path: Option>, pub(crate) computed_svg_transforms: Option, pub(crate) svg_viewport_size: Option, pub(crate) grid_layout_data: Option>, From a4406bd6ead1b67ee62451b67ee9295783dd945f Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 18:36:45 +0200 Subject: [PATCH 05/14] LibWeb: Drop the dead initial quote nesting level from NodeData The field had exactly one writer and no reader: the live quote nesting state flows through the tree builder's own state and the resolve-content callback arguments, so storing the initial level per node served nothing. Delete the field, its accessors, and the configure-layout-node callback parameter that existed only to feed it. The freed four bytes make room for widening the fragment cache epoch. --- Libraries/LibWeb/Layout/Node.h | 10 +++------- Libraries/LibWeb/Layout/TreeBuilder.cpp | 3 +-- Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs | 4 ++-- Libraries/LibWeb/Rust/src/layout/node_data.rs | 6 +++--- Libraries/LibWeb/Rust/src/layout/tree_builder.rs | 4 ++-- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 4701b6c92d1b0..c66509683b9a8 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -42,10 +42,9 @@ static_assert(offsetof(RustFFI::NodeData, kind) == 28); static_assert(offsetof(RustFFI::NodeData, generated_for) == 29); static_assert(offsetof(RustFFI::NodeData, intrinsic_cache_epoch) == 30); static_assert(offsetof(RustFFI::NodeData, flags) == 32); -static_assert(offsetof(RustFFI::NodeData, initial_quote_nesting_level) == 36); -static_assert(offsetof(RustFFI::NodeData, slot_generation) == 40); -static_assert(offsetof(RustFFI::NodeData, table_column_span) == 42); -static_assert(offsetof(RustFFI::NodeData, table_row_span) == 44); +static_assert(offsetof(RustFFI::NodeData, slot_generation) == 36); +static_assert(offsetof(RustFFI::NodeData, table_column_span) == 38); +static_assert(offsetof(RustFFI::NodeData, table_row_span) == 40); static_assert(offsetof(RustFFI::NodeData, style) == 48); static_assert(offsetof(RustFFI::NodeData, shell) == 56); @@ -262,9 +261,6 @@ class WEB_API Node bool is_editing_host() const { return has_flag(RustFFI::NodeFlag::IsEditingHost); } void set_is_editing_host(bool value) { set_flag(RustFFI::NodeFlag::IsEditingHost, value); } - u32 initial_quote_nesting_level() const { return m_data->initial_quote_nesting_level; } - void set_initial_quote_nesting_level(u32 value) { m_data->initial_quote_nesting_level = value; } - // https://drafts.csswg.org/css-ui/#propdef-user-select CSS::UserSelect user_select_used_value() const; diff --git a/Libraries/LibWeb/Layout/TreeBuilder.cpp b/Libraries/LibWeb/Layout/TreeBuilder.cpp index 322b35623e628..961452417cb1b 100644 --- a/Libraries/LibWeb/Layout/TreeBuilder.cpp +++ b/Libraries/LibWeb/Layout/TreeBuilder.cpp @@ -569,7 +569,7 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr VERIFY(frame.layout_node); auto marker_style = element.document().style_computer().compute_style({ element, CSS::PseudoElement::Marker }); (void)builder.create_and_attach_list_item_marker(as(*frame.layout_node), element, css_pseudo_element(originating_pseudo), move(marker_style)); }, - .configure_layout_node = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo, u32 initial_quote_nesting_level) { + .configure_layout_node = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo) { VERIFY(frame_pointer); VERIFY(element_pointer); auto& frame = *static_cast(frame_pointer); @@ -577,7 +577,6 @@ RustFFI::FfiPseudoTreeBuilderCallbacks LayoutTreeBuildBridge::make_ffi_pseudo_tr auto pseudo_element = css_pseudo_element(ffi_pseudo); VERIFY(frame.layout_node); frame.layout_node->set_generated_for(pseudo_element, element); - frame.layout_node->set_initial_quote_nesting_level(initial_quote_nesting_level); LayoutTreeBuilderAccess::set_synthetic_pseudo_element_node(element, pseudo_element, frame.layout_node); }, .resolve_content = [](void* frame_pointer, void* element_pointer, RustFFI::FfiPseudoElement ffi_pseudo, u32 initial_quote_nesting_level) -> RustFFI::FfiResolvedPseudoContentFacts { VERIFY(frame_pointer); diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index 692583d7ce8a6..a6bbb7b39abd4 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -1033,8 +1033,8 @@ mod tests { // SAFETY: The first allocation is still live, and the comparison above // confirms that its pointer still addresses the arena slot. unsafe { - (*first_data).initial_quote_nesting_level = 42; - assert_eq!((*arena.data(first.slot)).initial_quote_nesting_level, 42); + (*first_data).table_column_span = 42; + assert_eq!((*arena.data(first.slot)).table_column_span, 42); } arena.free(first.slot, first.generation); for allocation in allocations { diff --git a/Libraries/LibWeb/Rust/src/layout/node_data.rs b/Libraries/LibWeb/Rust/src/layout/node_data.rs index cfc0b99ba7e46..58d42ce5806dc 100644 --- a/Libraries/LibWeb/Rust/src/layout/node_data.rs +++ b/Libraries/LibWeb/Rust/src/layout/node_data.rs @@ -175,7 +175,6 @@ pub struct NodeData { pub generated_for: u8, pub intrinsic_cache_epoch: u16, pub flags: u32, - pub initial_quote_nesting_level: u32, pub slot_generation: u8, pub table_column_span: u16, pub table_row_span: u16, @@ -197,7 +196,6 @@ impl Default for NodeData { generated_for: 0, intrinsic_cache_epoch: 0, flags: 0, - initial_quote_nesting_level: 0, slot_generation: 0, table_column_span: 1, table_row_span: 1, @@ -222,7 +220,9 @@ mod tests { assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::offset_of!(NodeData, intrinsic_cache_epoch), 30); assert_eq!(std::mem::offset_of!(NodeData, flags), 32); - assert_eq!(std::mem::offset_of!(NodeData, slot_generation), 40); + assert_eq!(std::mem::offset_of!(NodeData, slot_generation), 36); + assert_eq!(std::mem::offset_of!(NodeData, table_column_span), 38); + assert_eq!(std::mem::offset_of!(NodeData, table_row_span), 40); assert_eq!(std::mem::offset_of!(NodeData, style), 48); assert_eq!(std::mem::offset_of!(NodeData, shell), 56); } diff --git a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs index 62022a2fab1aa..5c90877eb8fa4 100644 --- a/Libraries/LibWeb/Rust/src/layout/tree_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/tree_builder.rs @@ -1698,7 +1698,7 @@ pub struct FfiPseudoTreeBuilderCallbacks { pub attach_style_resources: unsafe extern "C" fn(*mut c_void), pub apply_replaced_display_adjustment: unsafe extern "C" fn(*mut c_void, FfiReplacedElementDisplayAdjustment), pub create_nested_list_marker: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, FfiPseudoElement), - pub configure_layout_node: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, u32), + pub configure_layout_node: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement), pub resolve_content: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, u32) -> FfiResolvedPseudoContentFacts, pub create_content_item: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiPseudoElement, usize) -> NodeSlotId, @@ -1830,7 +1830,7 @@ fn create_pseudo_element_with_frame( let initial_quote_nesting_level = state.quote_nesting_level; // SAFETY: The frame and element remain live throughout configuration. - unsafe { (callbacks.configure_layout_node)(frame, element, pseudo_element, initial_quote_nesting_level) }; + unsafe { (callbacks.configure_layout_node)(frame, element, pseudo_element) }; let layout_node_kind = host.layout().data(layout_node).kind; // https://drafts.csswg.org/css-lists-3/#list-style-position-outside // "the marker box is a block container and is placed outside the principal block box" From 926355bd4e1cdbc8f4a00d68fcc66e61e3868834 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 18:37:11 +0200 Subject: [PATCH 06/14] LibWeb: Make no-op style installs pointer-stable Two paths reinstalled computed values with intermediate or unconditionally cloned states even when nothing changed, defeating any consumer that uses style group payload identity to detect change: - propagate_overflow_to_viewport oscillated values within every full pass (viewport to auto, origin node to its element values, viewport to the applied values, origin node to visible), so a page whose html or body has non-visible overflow rewrote three nodes' box groups four times per pass forever. Compute the overflow origin and the applied values first and install each node's final values exactly once; a steady-state pass now leaves every group payload untouched via the setters' equality early-outs. - copy_grid_placements_from was the one mutator without an equality early-out: it cloned the target's grid group on every call. Compare the four placements (name-aware, since indices are interned per payload) and the placement style values first, and skip the clone when nothing would change. The epoch-based layout invalidation added next derives "this style change can affect layout" from group payload identity, which these paths would otherwise break. --- Libraries/LibWeb/CSS/ComputedValues.h | 8 ++-- Libraries/LibWeb/DOM/Document.cpp | 41 ++++++++--------- .../LibWeb/Rust/src/css/computed_values.rs | 46 +++++++++++++++++++ 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 4dc8190887288..aaa7527582a96 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -3178,9 +3178,11 @@ class ComputedValues::Mutator final { } void copy_grid_placements_from(ComputedValues const& source) { - ComputedValuesFFI::rust_grid_values_copy_placements( - static_cast(source.m_noninherited.grid.operator->()), - &m_values.m_noninherited.grid.access()); + auto const* source_grid = static_cast(source.m_noninherited.grid.operator->()); + auto const* current_grid = static_cast(m_values.m_noninherited.grid.operator->()); + if (ComputedValuesFFI::rust_grid_values_placements_equal(source_grid, current_grid)) + return; + ComputedValuesFFI::rust_grid_values_copy_placements(source_grid, &m_values.m_noninherited.grid.access()); } void reset_grid_placements_to_auto() { diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 0c26687f4772f..225830a224c73 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1844,11 +1844,6 @@ static void propagate_scrollbar_width_to_viewport(Element& root_element, Layout: // https://drafts.csswg.org/css-overflow-3/#overflow-propagation static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewport& viewport) { - viewport.modify_computed_values([](auto& values) { - values.set_overflow_x(CSS::Overflow::Auto); - values.set_overflow_y(CSS::Overflow::Auto); - }); - // https://drafts.csswg.org/css-contain-2/#contain-property // Additionally, when any containments are active on either the HTML or elements, propagation of // properties from the element to the initial containing block, the viewport, or the canvas background, is @@ -1866,19 +1861,8 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo // when the root element’s display value is not none. auto root_element_layout_node = root_element.unsafe_layout_node(); auto const& root_element_computed_values = *root_element.computed_values(); - root_element_layout_node->modify_computed_values([&](auto& values) { - values.set_overflow_x(root_element_computed_values.overflow_x()); - values.set_overflow_y(root_element_computed_values.overflow_y()); - }); - if (body_element_can_propagate_overflow) { - auto const& body_element_computed_values = *body_element->computed_values(); - body_element->unsafe_layout_node()->modify_computed_values([&](auto& values) { - values.set_overflow_x(body_element_computed_values.overflow_x()); - values.set_overflow_y(body_element_computed_values.overflow_y()); - }); - } - auto overflow_origin_node = root_element_layout_node; + Element* overflow_origin_element = &root_element; // However, when the root element is an [HTML] html element (including XML syntax for HTML) // whose overflow value is visible (in both axes), and that element has as a child @@ -1887,12 +1871,12 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo if (root_element.is_html_html_element() && !body_propagation_is_disabled_by_containment) { if (root_element_computed_values.overflow_x() == CSS::Overflow::Visible && root_element_computed_values.overflow_y() == CSS::Overflow::Visible) { if (body_element_can_propagate_overflow) - overflow_origin_node = body_element->unsafe_layout_node(); + overflow_origin_element = body_element; } } // If 'visible' is applied to the viewport, it must be interpreted as 'auto'. If 'clip' is applied to the viewport, it must be interpreted as 'hidden'. - auto const& overflow_origin_computed_values = overflow_origin_node->computed_values(); + auto const& overflow_origin_computed_values = *overflow_origin_element->computed_values(); auto overflow_x_to_apply = overflow_origin_computed_values.overflow_x(); if (overflow_x_to_apply == CSS::Overflow::Visible) { overflow_x_to_apply = CSS::Overflow::Auto; @@ -1905,17 +1889,30 @@ static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewpo } else if (overflow_y_to_apply == CSS::Overflow::Clip) { overflow_y_to_apply = CSS::Overflow::Hidden; } + // Every node receives its final values exactly once: a steady-state pass then leaves + // every style group payload untouched instead of oscillating values within the pass. viewport.modify_computed_values([&](auto& values) { values.set_overflow_x(overflow_x_to_apply); values.set_overflow_y(overflow_y_to_apply); }); + // UAs must apply the overflow-* values set on the root element to the viewport + // when the root element's display value is not none. // The element from which the value is propagated must then have a used overflow value of visible. // FIXME: Apply this to the used values, not the computed ones. - overflow_origin_node->modify_computed_values([](auto& values) { - values.set_overflow_x(CSS::Overflow::Visible); - values.set_overflow_y(CSS::Overflow::Visible); + bool root_element_is_overflow_origin = overflow_origin_element == &root_element; + root_element_layout_node->modify_computed_values([&](auto& values) { + values.set_overflow_x(root_element_is_overflow_origin ? CSS::Overflow::Visible : root_element_computed_values.overflow_x()); + values.set_overflow_y(root_element_is_overflow_origin ? CSS::Overflow::Visible : root_element_computed_values.overflow_y()); }); + if (body_element_can_propagate_overflow) { + bool body_element_is_overflow_origin = overflow_origin_element == body_element; + auto const& body_element_computed_values = *body_element->computed_values(); + body_element->unsafe_layout_node()->modify_computed_values([&](auto& values) { + values.set_overflow_x(body_element_is_overflow_origin ? CSS::Overflow::Visible : body_element_computed_values.overflow_x()); + values.set_overflow_y(body_element_is_overflow_origin ? CSS::Overflow::Visible : body_element_computed_values.overflow_y()); + }); + } } void Document::update_layout_if_needed_for_node(Node const& node, UpdateLayoutReason reason) diff --git a/Libraries/LibWeb/Rust/src/css/computed_values.rs b/Libraries/LibWeb/Rust/src/css/computed_values.rs index c88eccb1dafe5..7927b3681ce6d 100644 --- a/Libraries/LibWeb/Rust/src/css/computed_values.rs +++ b/Libraries/LibWeb/Rust/src/css/computed_values.rs @@ -2056,6 +2056,52 @@ pub unsafe extern "C" fn rust_grid_values_copy_placements(source: *const GridVal }); } +/// # Safety +/// `source` and `target` must be valid grid group payloads. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rust_grid_values_placements_equal( + source: *const GridValues, + target: *const GridValues, +) -> bool { + abort_on_panic(|| { + // SAFETY: The caller passes valid payloads and only reads them. + let (source, target) = unsafe { (&*source, &*target) }; + let name_raw = |grid: &GridValues, index: u32| { + if index == GRID_NO_INDEX { + return 0; + } + grid.names.as_slice()[index as usize].raw() + }; + // Name indices are payload-local, so a placement compares as its index-neutralized + // shape plus the raw names those indices resolve to; a field added to + // ComputedGridPlacement flows into the comparison through the struct update. + let comparable_placement = |grid: &GridValues, placement: &ComputedGridPlacement| { + ( + ComputedGridPlacement { + name_index: GRID_NO_INDEX, + implicit_start_name_index: GRID_NO_INDEX, + implicit_end_name_index: GRID_NO_INDEX, + ..*placement + }, + name_raw(grid, placement.name_index), + name_raw(grid, placement.implicit_start_name_index), + name_raw(grid, placement.implicit_end_name_index), + ) + }; + let placements_equal = |ours: &ComputedGridPlacement, theirs: &ComputedGridPlacement| { + comparable_placement(source, ours) == comparable_placement(target, theirs) + }; + placements_equal(&source.column_start, &target.column_start) + && placements_equal(&source.column_end, &target.column_end) + && placements_equal(&source.row_start, &target.row_start) + && placements_equal(&source.row_end, &target.row_end) + && source.grid_column_start_style_value == target.grid_column_start_style_value + && source.grid_column_end_style_value == target.grid_column_end_style_value + && source.grid_row_start_style_value == target.grid_row_start_style_value + && source.grid_row_end_style_value == target.grid_row_end_style_value + }) +} + /// # Safety /// `target` must be a uniquely owned grid group value. #[unsafe(no_mangle)] From 0bd965832b1f9c8afbdda7cea5a4d9df7bf6d240 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:05:48 +0200 Subject: [PATCH 07/14] LibWeb: Add fragment cache epochs for layout invalidation tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The formatting-context run cache added next validates entries across layout passes, and the existing intrinsic cache epoch cannot carry that job: its ancestor walk stops at absolutely positioned and SVG roots and the boundary-self-only path skips ancestors entirely, while cached fragment trees contain those descendants' fragments. Add a per-node fragment cache epoch, wide enough that wrapping between a store and the next probe is unreachable, advanced with no propagation boundary wherever layout becomes invalid: - New computed values landing on a layout node. Every style path funnels through set_computed_values — element restyles, inherited recomputation including the animation fast path's descendant walk, pseudo-element application, and anonymous wrapper propagation at any depth — so the funnel decides "this change can affect layout" by comparing the layout-affecting style group payload pointers with a value-equality fallback (inherited recomputation does not canonicalize against the node's previous style), and treats animated values as always layout-affecting through the same overlay predicate the style differ's group fast path uses. Invalidation call sites alone missed the animation fast path's descendant restyles and anonymous wrappers below depth one. The group list, each group's member path, and its layout-affecting classification live in the one X-macro that generates every group walker, so a future group cannot be forgotten by any of them; background, mask, and text reset hold only values read at paint or display-list build time and do not bump. - set_needs_layout_update, for invalidation reasons that are not style changes: self, generated anonymous children, and the full ancestor chain, including the boundary-self-only path. The bump runs before the already-dirty early return, because a dirty node does not imply its ancestor chain was bumped for the current epoch values and over-bumping is free. - The image-data change walk. - The tree mutation primitives: layout tree restructuring never funnels through set_needs_layout_update, so RefCountedTreeNode notes every structural change on the mutation parent. The walks are gated on the cache's environment variable and no-op otherwise; nothing reads the epoch yet. Bumps can legitimately land while another document's layout pass is on the stack — a parent pass sizing a child navigable's viewport invalidates the child document — so the helpers must not assert against the process-global pass flag; an invalidation raised mid-pass is instead defeated by the cache's probe-time validity capture. --- Libraries/LibWeb/CSS/ComputedValues.cpp | 95 +++++-------------- Libraries/LibWeb/CSS/ComputedValues.h | 95 +++++++++---------- Libraries/LibWeb/DOM/Element.cpp | 3 +- Libraries/LibWeb/HTML/HTMLImageElement.cpp | 1 + Libraries/LibWeb/Layout/Node.cpp | 47 +++++++++ Libraries/LibWeb/Layout/Node.h | 21 +++- Libraries/LibWeb/RefCountedTreeNode.h | 8 ++ Libraries/LibWeb/Rust/src/layout/node_data.rs | 15 ++- 8 files changed, 156 insertions(+), 129 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 1b8b75267eed8..6bd61a09038c0 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -751,7 +751,8 @@ void const* style_group_default_payload(size_t group_index) static auto const default_payloads = [] { constexpr auto group_count = to_underlying(StyleGroupIndex::Count); Array vtables; -#define LIBWEB_STYLE_GROUP_VTABLE(name) vtables[to_underlying(StyleGroupIndex::name)] = make_style_group_vtable(); +#define LIBWEB_STYLE_GROUP_VTABLE(name, path, sharing_name, affects_layout) \ + vtables[to_underlying(StyleGroupIndex::name)] = make_style_group_vtable(); LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_VTABLE) #undef LIBWEB_STYLE_GROUP_VTABLE Array payloads {}; @@ -853,34 +854,27 @@ bool ComputedValues::adopt_identical_group_payloads(ComputedValues const& previo } all_shared = false; }; -#define LIBWEB_ADOPT_STYLE_GROUP(path) adopt(path, previous.path); - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.table) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.list) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.ui) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.svg) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.text) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.box) - LIBWEB_ADOPT_STYLE_GROUP(m_inherited.font) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.animation) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.box) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.surround) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.sizing) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.misc) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.alignment) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.border) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.background) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.transform) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.effects) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.mask_data) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.text_reset) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.content_data) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.anchor) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.grid) - LIBWEB_ADOPT_STYLE_GROUP(m_noninherited.svg_reset) +#define LIBWEB_ADOPT_STYLE_GROUP(name, path, sharing_name, affects_layout) adopt(path, previous.path); + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_ADOPT_STYLE_GROUP) #undef LIBWEB_ADOPT_STYLE_GROUP return all_shared; } +bool ComputedValues::differs_in_any_layout_affecting_group_payload_from(ComputedValues const& other) const +{ + auto differs = [](StyleStructRef const& mine, StyleStructRef const& theirs) { + return !mine.ptr_equals(theirs) && !(mine == theirs); + }; +#define LIBWEB_COMPARE_STYLE_GROUP(name, path, sharing_name, affects_layout) \ + if constexpr (affects_layout) { \ + if (differs(path, other.path)) \ + return true; \ + } + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_COMPARE_STYLE_GROUP) +#undef LIBWEB_COMPARE_STYLE_GROUP + return false; +} + // https://drafts.csswg.org/css-transforms-2/#grouping-property-values bool ComputedValues::has_transform_style_grouping_property() const { @@ -935,52 +929,11 @@ bool ComputedValues::has_transform_style_grouping_property() const void const* ComputedValues::style_group_payload(StyleGroupIndex group) const { switch (group) { - case StyleGroupIndex::InheritedTableValues: - return &*m_inherited.table; - case StyleGroupIndex::InheritedListValues: - return &*m_inherited.list; - case StyleGroupIndex::InheritedUIValues: - return &*m_inherited.ui; - case StyleGroupIndex::InheritedSVGValues: - return &*m_inherited.svg; - case StyleGroupIndex::InheritedTextValues: - return &*m_inherited.text; - case StyleGroupIndex::InheritedBoxValues: - return &*m_inherited.box; - case StyleGroupIndex::FontValues: - return &*m_inherited.font; - case StyleGroupIndex::AnimationValues: - return &*m_noninherited.animation; - case StyleGroupIndex::SVGResetValues: - return &*m_noninherited.svg_reset; - case StyleGroupIndex::GridValues: - return &*m_noninherited.grid; - case StyleGroupIndex::AnchorValues: - return &*m_noninherited.anchor; - case StyleGroupIndex::EffectsValues: - return &*m_noninherited.effects; - case StyleGroupIndex::MaskValues: - return &*m_noninherited.mask_data; - case StyleGroupIndex::TextResetValues: - return &*m_noninherited.text_reset; - case StyleGroupIndex::ContentValues: - return &*m_noninherited.content_data; - case StyleGroupIndex::TransformValues: - return &*m_noninherited.transform; - case StyleGroupIndex::BackgroundValues: - return &*m_noninherited.background; - case StyleGroupIndex::BorderValues: - return &*m_noninherited.border; - case StyleGroupIndex::AlignmentValues: - return &*m_noninherited.alignment; - case StyleGroupIndex::MiscResetValues: - return &*m_noninherited.misc; - case StyleGroupIndex::SizingValues: - return &*m_noninherited.sizing; - case StyleGroupIndex::SurroundValues: - return &*m_noninherited.surround; - case StyleGroupIndex::BoxValues: - return &*m_noninherited.box; +#define LIBWEB_STYLE_GROUP_PAYLOAD_CASE(name, path, sharing_name, affects_layout) \ + case StyleGroupIndex::name: \ + return &*path; + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_PAYLOAD_CASE) +#undef LIBWEB_STYLE_GROUP_PAYLOAD_CASE case StyleGroupIndex::Count: break; } diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index aaa7527582a96..73d0450e55e9b 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -896,34 +896,42 @@ inline Gfx::InterpolationColorSpace to_interpolation_color_space(ColorInterpolat VERIFY_NOT_REACHED(); } -// The identity of every ComputedValues style value group, in vtable registration order. -#define LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(G) \ - G(InheritedTableValues) \ - G(InheritedListValues) \ - G(InheritedUIValues) \ - G(InheritedSVGValues) \ - G(InheritedTextValues) \ - G(InheritedBoxValues) \ - G(FontValues) \ - G(AnimationValues) \ - G(SVGResetValues) \ - G(GridValues) \ - G(AnchorValues) \ - G(EffectsValues) \ - G(MaskValues) \ - G(TextResetValues) \ - G(ContentValues) \ - G(TransformValues) \ - G(BackgroundValues) \ - G(BorderValues) \ - G(AlignmentValues) \ - G(MiscResetValues) \ - G(SizingValues) \ - G(SurroundValues) \ - G(BoxValues) +// Every ComputedValues style value group, in vtable registration order: +// G(enumerator, member path, sharing-info name, affects layout). +// A group may be flagged as not affecting layout only when every one of its fields is +// read exclusively at paint or display-list build time: background qualifies (paint; +// resource-observer registration runs in apply_style regardless of layout), mask +// qualifies (paint and hit-testing), and text_reset qualifies (text-decoration resolves +// at display-list build; white-space-trim is unread by layout today, so implementing it +// must revisit the flag). effects stays layout-affecting because filter/backdrop-filter +// establish fixed-positioning containing blocks and re-parent abspos descendants. +#define LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(G) \ + G(InheritedTableValues, m_inherited.table, "inheritedTable", true) \ + G(InheritedListValues, m_inherited.list, "inheritedList", true) \ + G(InheritedUIValues, m_inherited.ui, "inheritedUI", true) \ + G(InheritedSVGValues, m_inherited.svg, "inheritedSVG", true) \ + G(InheritedTextValues, m_inherited.text, "inheritedText", true) \ + G(InheritedBoxValues, m_inherited.box, "inheritedBox", true) \ + G(FontValues, m_inherited.font, "font", true) \ + G(AnimationValues, m_noninherited.animation, "animation", true) \ + G(SVGResetValues, m_noninherited.svg_reset, "svgReset", true) \ + G(GridValues, m_noninherited.grid, "grid", true) \ + G(AnchorValues, m_noninherited.anchor, "anchor", true) \ + G(EffectsValues, m_noninherited.effects, "effects", true) \ + G(MaskValues, m_noninherited.mask_data, "mask", false) \ + G(TextResetValues, m_noninherited.text_reset, "textReset", false) \ + G(ContentValues, m_noninherited.content_data, "content", true) \ + G(TransformValues, m_noninherited.transform, "transform", true) \ + G(BackgroundValues, m_noninherited.background, "background", false) \ + G(BorderValues, m_noninherited.border, "border", true) \ + G(AlignmentValues, m_noninherited.alignment, "alignment", true) \ + G(MiscResetValues, m_noninherited.misc, "miscReset", true) \ + G(SizingValues, m_noninherited.sizing, "sizing", true) \ + G(SurroundValues, m_noninherited.surround, "surround", true) \ + G(BoxValues, m_noninherited.box, "box", true) enum class StyleGroupIndex : size_t { -#define LIBWEB_STYLE_GROUP_ENUMERATOR(name) name, +#define LIBWEB_STYLE_GROUP_ENUMERATOR(name, ...) name, LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_STYLE_GROUP_ENUMERATOR) #undef LIBWEB_STYLE_GROUP_ENUMERATOR Count, @@ -1049,6 +1057,13 @@ class WEB_API ComputedValues final : public RefCounted { AnimatedProperties const* animated_properties() const { return m_animated_properties.ptr(); } RefPtr animated_properties_snapshot() const; + // Animated values live outside the group payloads, so every group-based fast path or + // group-based diff must fall back to the slow path when either side carries them. + static bool either_carries_animated_overlay(ComputedValues const& a, ComputedValues const& b) + { + return a.has_animated_values() || b.has_animated_values() || a.animated_properties() || b.animated_properties(); + } + struct Statistics { u64 live_instance_count { 0 }; u64 total_instances_created { 0 }; @@ -1061,6 +1076,7 @@ class WEB_API ComputedValues final : public RefCounted { // restyled element keep sharing storage across style generations. Returns true when every // group ends up sharing its payload with `previous`. bool adopt_identical_group_payloads(ComputedValues const& previous) const; + bool differs_in_any_layout_affecting_group_payload_from(ComputedValues const& other) const; bool has_transform_style_grouping_property() const; @@ -1075,30 +1091,9 @@ class WEB_API ComputedValues final : public RefCounted { template void for_each_style_group_sharing_state(ComputedValues const* parent, Callback callback) const { -#define LIBWEB_VISIT_STYLE_GROUP(name, path) callback(name##sv, parent ? path.ptr_equals(parent->path) : false, path.is_default()); - LIBWEB_VISIT_STYLE_GROUP("inheritedTable", m_inherited.table) - LIBWEB_VISIT_STYLE_GROUP("inheritedList", m_inherited.list) - LIBWEB_VISIT_STYLE_GROUP("inheritedUI", m_inherited.ui) - LIBWEB_VISIT_STYLE_GROUP("inheritedSVG", m_inherited.svg) - LIBWEB_VISIT_STYLE_GROUP("inheritedText", m_inherited.text) - LIBWEB_VISIT_STYLE_GROUP("inheritedBox", m_inherited.box) - LIBWEB_VISIT_STYLE_GROUP("font", m_inherited.font) - LIBWEB_VISIT_STYLE_GROUP("animation", m_noninherited.animation) - LIBWEB_VISIT_STYLE_GROUP("box", m_noninherited.box) - LIBWEB_VISIT_STYLE_GROUP("surround", m_noninherited.surround) - LIBWEB_VISIT_STYLE_GROUP("sizing", m_noninherited.sizing) - LIBWEB_VISIT_STYLE_GROUP("miscReset", m_noninherited.misc) - LIBWEB_VISIT_STYLE_GROUP("alignment", m_noninherited.alignment) - LIBWEB_VISIT_STYLE_GROUP("border", m_noninherited.border) - LIBWEB_VISIT_STYLE_GROUP("background", m_noninherited.background) - LIBWEB_VISIT_STYLE_GROUP("transform", m_noninherited.transform) - LIBWEB_VISIT_STYLE_GROUP("effects", m_noninherited.effects) - LIBWEB_VISIT_STYLE_GROUP("mask", m_noninherited.mask_data) - LIBWEB_VISIT_STYLE_GROUP("textReset", m_noninherited.text_reset) - LIBWEB_VISIT_STYLE_GROUP("content", m_noninherited.content_data) - LIBWEB_VISIT_STYLE_GROUP("anchor", m_noninherited.anchor) - LIBWEB_VISIT_STYLE_GROUP("grid", m_noninherited.grid) - LIBWEB_VISIT_STYLE_GROUP("svgReset", m_noninherited.svg_reset) +#define LIBWEB_VISIT_STYLE_GROUP(name, path, sharing_name, affects_layout) \ + callback(sharing_name##sv, parent ? path.ptr_equals(parent->path) : false, path.is_default()); + LIBWEB_ENUMERATE_COMPUTED_VALUE_STYLE_GROUPS(LIBWEB_VISIT_STYLE_GROUP) #undef LIBWEB_VISIT_STYLE_GROUP } diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index a22ee402cacc5..b147654312f58 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -1231,8 +1231,7 @@ static CSS::RequiredInvalidationAfterStyleChange compute_required_invalidation(C // previous style generation, which future diffs turn into pure pointer compares. bool const all_group_payloads_shared = new_computed_values.adopt_identical_group_payloads(old_computed_values); bool const property_diff_can_be_skipped = all_group_payloads_shared - && !old_computed_values.has_animated_values() && !new_computed_values.has_animated_values() - && !old_computed_values.animated_properties() && !new_computed_values.animated_properties(); + && !CSS::ComputedValues::either_carries_animated_overlay(old_computed_values, new_computed_values); static bool const verify_fast_path = getenv("LIBWEB_VERIFY_STYLE_DIFF_FAST_PATH") != nullptr; if (!property_diff_can_be_skipped || verify_fast_path) { diff --git a/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Libraries/LibWeb/HTML/HTMLImageElement.cpp index b83b040bfcffd..40cd2fba44d5d 100644 --- a/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -125,6 +125,7 @@ static bool image_element_dimensions_may_depend_on_intrinsic_size(Layout::ImageB static void reset_intrinsic_size_caches_after_image_data_change(Layout::ImageBox& image_box) { + image_box.bump_fragment_cache_epoch_of_self_and_ancestors(); image_box.reset_cached_intrinsic_sizes(); for (auto* ancestor = image_box.parent(); ancestor; ancestor = ancestor->parent()) { auto* box = as_if(ancestor); diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 387b4e81d7467..f13c2c7449ebb 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -114,6 +114,32 @@ void Node::enroll_for_arena_replaced_content_facts_sync_if_eligible() node_arena().enroll_node_for_replaced_content_facts_sync(*this); } +bool Node::fragment_cache_epochs_enabled() +{ + static bool const enabled = getenv("LADYBIRD_FC_RUN_CACHE") != nullptr; + return enabled; +} + +void Node::bump_fragment_cache_epoch() +{ + if (!fragment_cache_epochs_enabled()) + return; + ++node_data().fragment_cache_epoch; +} + +// NB: Bumps can legitimately run while another document's layout pass is on the stack (a +// parent pass sizing a child navigable's viewport invalidates the child document), so the +// helpers must not assert against the process-global pass flag. A bump landing between a +// run's probe and its store is handled by storing the probe-time validity, which turns it +// into a fail-safe miss. +void Node::bump_fragment_cache_epoch_of_self_and_ancestors() +{ + if (!fragment_cache_epochs_enabled()) + return; + for (auto* node = this; node; node = node->parent_ptr()) + ++node->node_data().fragment_cache_epoch; +} + void* Node::arena_handle() const { return m_arena->handle(); @@ -1009,12 +1035,28 @@ NonnullRefPtr NodeWithStyle::create_anonymous_wrapper() const void NodeWithStyle::set_computed_values(NonnullRefPtr computed_values) { VERIFY(!layout_pass_currently_running()); + + // Every path that lands computed values on a layout node funnels through here — element + // restyles, inherited-style recomputation (including the animation fast path's descendant + // walk), pseudo-element application, and anonymous wrapper propagation at any depth — so + // this is the one place that can tell whether a style change can affect this box's layout. + // Style-side layout inputs are exactly the layout-affecting group payloads published to + // node data plus the animated-value overlay, which lives outside the groups and + // disqualifies pointer diffing the same way it disqualifies the style differ's group + // fast path. + bool const changes_layout_affecting_style = fragment_cache_epochs_enabled() + && (CSS::ComputedValues::either_carries_animated_overlay(*m_computed_values, *computed_values) + || computed_values->differs_in_any_layout_affecting_group_payload_from(*m_computed_values)); + m_computed_values = move(computed_values); set_flag(RustFFI::NodeFlag::HasAnchorNames, !m_computed_values->anchor_names().is_empty()); set_flag(RustFFI::NodeFlag::InsetsUseAnchorFunctions, m_computed_values->inset_properties_contain_anchor_functions()); publish_style_container_to_node_data(); enroll_for_arena_replaced_content_facts_sync_if_eligible(); + if (changes_layout_affecting_style) + bump_fragment_cache_epoch_of_self_and_ancestors(); + for (auto* child = first_child_ptr(); child; child = child->next_sibling_ptr()) { if (auto* text_child = as_if(*child)) text_child->enroll_for_arena_text_content_sync(); @@ -1395,6 +1437,10 @@ bool NodeWithStyle::has_paint_containment() const void Node::set_needs_layout_update(DOM::SetNeedsLayoutReason reason, LayoutUpdatePropagation propagation) { + // Bumped before the already-dirty early return below: a dirty node does not imply its + // whole ancestor chain was bumped for the current epoch values, and over-bumping is free. + bump_fragment_cache_epoch_of_self_and_ancestors(); + if (needs_layout_update() && propagation == LayoutUpdatePropagation::ThroughAncestors) { // A dirty node normally implies dirty ancestors, but the walk that marked a partial // relayout boundary stopped there and left its ancestors clean, so a through-ancestors @@ -1422,6 +1468,7 @@ void Node::set_needs_layout_update(DOM::SetNeedsLayoutReason reason, LayoutUpdat // NOTE: if this node generated an anonymous parent, all ancestors are indiscriminately marked below. for_each_child_of_type([&](Box& child) { if (child.is_anonymous() && !is(child)) { + child.bump_fragment_cache_epoch(); child.set_flag(RustFFI::NodeFlag::NeedsLayoutUpdate, true); child.reset_cached_intrinsic_sizes(); } diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index c66509683b9a8..b529996d794de 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -42,9 +42,10 @@ static_assert(offsetof(RustFFI::NodeData, kind) == 28); static_assert(offsetof(RustFFI::NodeData, generated_for) == 29); static_assert(offsetof(RustFFI::NodeData, intrinsic_cache_epoch) == 30); static_assert(offsetof(RustFFI::NodeData, flags) == 32); -static_assert(offsetof(RustFFI::NodeData, slot_generation) == 36); -static_assert(offsetof(RustFFI::NodeData, table_column_span) == 38); -static_assert(offsetof(RustFFI::NodeData, table_row_span) == 40); +static_assert(offsetof(RustFFI::NodeData, fragment_cache_epoch) == 36); +static_assert(offsetof(RustFFI::NodeData, slot_generation) == 40); +static_assert(offsetof(RustFFI::NodeData, table_column_span) == 42); +static_assert(offsetof(RustFFI::NodeData, table_row_span) == 44); static_assert(offsetof(RustFFI::NodeData, style) == 48); static_assert(offsetof(RustFFI::NodeData, shell) == 56); @@ -113,6 +114,20 @@ class WEB_API Node bool needs_layout_update() const { return has_flag(RustFFI::NodeFlag::NeedsLayoutUpdate); } + // The formatting-context run cache (LADYBIRD_FC_RUN_CACHE) validates its entries against + // these epochs; with the cache disabled nothing reads them, so the walks no-op. + static bool fragment_cache_epochs_enabled(); + + void bump_fragment_cache_epoch(); + + // Any invalidation or restructuring below a node must reach every ancestor's epoch: cached + // runs capture subtree structure, and unlike intrinsic-size invalidation there is no + // absolutely-positioned or SVG boundary — those descendants' fragments live in ancestor + // run trees. Layout tree restructuring in particular never funnels through + // set_needs_layout_update (a full pass lays out everything), so the tree mutation + // primitives call this on the parent of every structural change. + void bump_fragment_cache_epoch_of_self_and_ancestors(); + // Set when a style change altered geometry-determining properties of this node itself, so // a partial relayout must re-resolve its own size and position instead of reusing them. bool needs_own_geometry_update() const { return has_flag(RustFFI::NodeFlag::NeedsOwnGeometryUpdate); } diff --git a/Libraries/LibWeb/RefCountedTreeNode.h b/Libraries/LibWeb/RefCountedTreeNode.h index 8f44c2f037903..c2cade6b2adfc 100644 --- a/Libraries/LibWeb/RefCountedTreeNode.h +++ b/Libraries/LibWeb/RefCountedTreeNode.h @@ -282,6 +282,7 @@ class WEB_API RefCountedTreeNode { if (next_sibling) next_sibling->synchronize_topology(); } + note_structural_change_to_layout_caches(); } void replace_child(NonnullRefPtr new_child, T& old_child) @@ -621,6 +622,13 @@ class WEB_API RefCountedTreeNode { if (auto* next_sibling = node.next_sibling_ptr()) next_sibling->synchronize_topology(); } + note_structural_change_to_layout_caches(); + } + + void note_structural_change_to_layout_caches() + { + if constexpr (requires { static_cast(*this).bump_fragment_cache_epoch_of_self_and_ancestors(); }) + static_cast(*this).bump_fragment_cache_epoch_of_self_and_ancestors(); } WeakPtr m_parent; diff --git a/Libraries/LibWeb/Rust/src/layout/node_data.rs b/Libraries/LibWeb/Rust/src/layout/node_data.rs index 58d42ce5806dc..1d85095e98e40 100644 --- a/Libraries/LibWeb/Rust/src/layout/node_data.rs +++ b/Libraries/LibWeb/Rust/src/layout/node_data.rs @@ -175,6 +175,13 @@ pub struct NodeData { pub generated_for: u8, pub intrinsic_cache_epoch: u16, pub flags: u32, + /// Advanced on every layout invalidation that reaches this node or its + /// subtree, with no propagation boundary: unlike the intrinsic epoch, + /// changes inside absolutely positioned and SVG descendants must reach + /// every ancestor, because their fragments live in ancestor run trees. + /// Wide enough that wrapping between a cache store and the next probe + /// is unreachable. + pub fragment_cache_epoch: u32, pub slot_generation: u8, pub table_column_span: u16, pub table_row_span: u16, @@ -199,6 +206,7 @@ impl Default for NodeData { slot_generation: 0, table_column_span: 1, table_row_span: 1, + fragment_cache_epoch: 0, style: std::ptr::null(), shell: std::ptr::null_mut(), } @@ -220,9 +228,10 @@ mod tests { assert_eq!(std::mem::size_of::(), 64); assert_eq!(std::mem::offset_of!(NodeData, intrinsic_cache_epoch), 30); assert_eq!(std::mem::offset_of!(NodeData, flags), 32); - assert_eq!(std::mem::offset_of!(NodeData, slot_generation), 36); - assert_eq!(std::mem::offset_of!(NodeData, table_column_span), 38); - assert_eq!(std::mem::offset_of!(NodeData, table_row_span), 40); + assert_eq!(std::mem::offset_of!(NodeData, fragment_cache_epoch), 36); + assert_eq!(std::mem::offset_of!(NodeData, slot_generation), 40); + assert_eq!(std::mem::offset_of!(NodeData, table_column_span), 42); + assert_eq!(std::mem::offset_of!(NodeData, table_row_span), 44); assert_eq!(std::mem::offset_of!(NodeData, style), 48); assert_eq!(std::mem::offset_of!(NodeData, shell), 56); } From 2d0d19862e5306d579664732d782b7103ed8b533 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 22:46:47 +0200 Subject: [PATCH 08/14] LibWeb: Invalidate layout when replaced-content facts change The default preferred size of a text control follows the input size and textarea rows/cols attributes, but those attributes are not presentational hints and had no invalidation at all: changing them left layout untouched until an unrelated pass happened to run, and a cached formatting-context run would keep the stale size indefinitely. Invalidate at both layers. The attribute-change sites mark the control's layout node for update, and the per-pass replaced-content facts sync compares against the previously stored facts and bumps the fragment cache epochs whenever they changed, covering every facts source uniformly, including channels with no invalidation of their own such as SVG root and navigable-container natural metrics. Select's size attribute is outside the facts channel and keeps its pre-existing gap. Covered by regression tests that grow an input via size and a textarea via rows and assert the box resizes. --- Libraries/LibWeb/Layout/NodeArena.cpp | 7 +++++-- Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs | 11 +++++++---- Libraries/LibWeb/Rust/src/layout/node_data.rs | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 631cb12a4618e..5e5667958264c 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -78,7 +78,7 @@ void NodeArena::sync_enrolled_replaced_content_facts() { bool any_enrolled_node_died = false; for (auto& weak_node : m_nodes_enrolled_for_replaced_content_facts_sync) { - auto const* node = weak_node.ptr(); + auto* node = weak_node.ptr(); if (!node) { any_enrolled_node_died = true; continue; @@ -86,7 +86,10 @@ void NodeArena::sync_enrolled_replaced_content_facts() RustFFI::FfiReplacedContentFacts facts {}; if (auto const* box = as_if(*node)) facts = box->build_replaced_content_facts_for_arena(); - RustFFI::layout_arena_set_replaced_content_facts(m_handle, Node::slot_id(node), facts); + // Changed facts invalidate cached formatting-context runs regardless of which + // channel produced the change, including sources with no invalidation of their own. + if (RustFFI::layout_arena_set_replaced_content_facts(m_handle, Node::slot_id(node), facts)) + node->bump_fragment_cache_epoch_of_self_and_ancestors(); } if (any_enrolled_node_died) m_nodes_enrolled_for_replaced_content_facts_sync.remove_all_matching([](auto& weak_node) { return !weak_node.ptr(); }); diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index a6bbb7b39abd4..e1c67f95738c9 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -709,7 +709,7 @@ impl LayoutNodeArena { } } - pub(crate) fn set_replaced_content_facts(&mut self, id: NodeSlotId, facts: FfiReplacedContentFacts) { + pub(crate) fn set_replaced_content_facts(&mut self, id: NodeSlotId, facts: FfiReplacedContentFacts) -> bool { self.assert_owner_thread(); self.data(id); let index = id.slot_index() as usize; @@ -717,10 +717,13 @@ impl LayoutNodeArena { self.replaced_content_facts .resize_with(index + 1, ReplacedContentFactsSlot::default); } + let previous = &self.replaced_content_facts[index]; + let changed = previous.generation != id.generation() || previous.facts != Some(facts); self.replaced_content_facts[index] = ReplacedContentFactsSlot { generation: id.generation(), facts: Some(facts), }; + changed } pub(crate) fn replaced_content_facts(&self, id: NodeSlotId) -> Option { @@ -987,13 +990,13 @@ pub unsafe extern "C" fn layout_arena_set_replaced_content_facts( arena: *mut c_void, id: NodeSlotId, facts: FfiReplacedContentFacts, -) { +) -> bool { abort_on_panic(|| { assert!(!arena.is_null(), "layout node arena handle is null"); // SAFETY: The C++ wrapper keeps the arena alive for this call and // serializes all access on the document thread. - unsafe { &mut *arena.cast::() }.set_replaced_content_facts(id, facts); - }); + unsafe { &mut *arena.cast::() }.set_replaced_content_facts(id, facts) + }) } #[unsafe(no_mangle)] diff --git a/Libraries/LibWeb/Rust/src/layout/node_data.rs b/Libraries/LibWeb/Rust/src/layout/node_data.rs index 1d85095e98e40..ba3e85f117bd5 100644 --- a/Libraries/LibWeb/Rust/src/layout/node_data.rs +++ b/Libraries/LibWeb/Rust/src/layout/node_data.rs @@ -14,7 +14,7 @@ pub const GENERATED_FOR_MARKER: u8 = 6; // count so the style container array and the registered group indices line up. pub const STYLE_GROUP_COUNT: usize = 23; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, PartialEq, Eq)] #[repr(C)] pub struct FfiReplacedContentFacts { pub has_auto_content_width: bool, From c96efd381fa9cb04ac2401f377b1f0e238c8dfab Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 22:46:47 +0200 Subject: [PATCH 09/14] LibWeb: Invalidate layout when rendered text changes under lang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendered text under a casing text-transform is keyed on the language for locale-sensitive casing, but lang reaches no computed style group, so the language invalidator's style-only walk never marked layout dirty, and the eventual arena text resync replaced the stored buffer without bumping any epoch: a cached formatting-context run would replay the old casing, holding line data that pointed into the freed text buffer. Invalidate at both layers. The language invalidator marks text nodes under casing transforms for re-rendering and relayout, rebuilding the layout tree for first-letter slices whose boundaries depend on the rendered text, and the arena text setter compares old and new content and reports change so the sync site bumps the fragment cache epochs for any rendered-text change regardless of which channel produced it. Covered by a layout test flipping lang from en to tr under text-transform: uppercase, which must re-render with the dotted capital İ. --- Libraries/LibWeb/Layout/NodeArena.cpp | 8 +++++-- Libraries/LibWeb/Layout/TextNode.cpp | 7 +++--- Libraries/LibWeb/Layout/TextNode.h | 2 +- .../Rust/src/layout/layout_node_arena.rs | 22 +++++++++++++++---- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 5e5667958264c..9732a29463ca6 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -49,14 +49,18 @@ void NodeArena::sync_enrolled_text_node_content() // by a later tree update without another enrollment trigger. Vector> still_detached_text_nodes; for (auto& weak_text_node : m_text_nodes_enrolled_for_content_sync) { - auto const* text_node = weak_text_node.ptr(); + auto* text_node = weak_text_node.ptr(); if (!text_node) continue; if (!text_node->parent()) { still_detached_text_nodes.append(move(weak_text_node)); continue; } - text_node->sync_text_content_to_arena(); + // Changed rendered text invalidates cached formatting-context runs regardless of + // which channel produced the change, including sources with no invalidation of + // their own (e.g. lang-keyed locale-sensitive casing). + if (text_node->sync_text_content_to_arena()) + text_node->bump_fragment_cache_epoch_of_self_and_ancestors(); } m_text_nodes_enrolled_for_content_sync = move(still_detached_text_nodes); } diff --git a/Libraries/LibWeb/Layout/TextNode.cpp b/Libraries/LibWeb/Layout/TextNode.cpp index b9002ca9b0269..73c1d96803091 100644 --- a/Libraries/LibWeb/Layout/TextNode.cpp +++ b/Libraries/LibWeb/Layout/TextNode.cpp @@ -424,14 +424,14 @@ void TextNode::enroll_for_arena_text_content_sync() const node_arena().enroll_text_node_for_content_sync(*this); } -void TextNode::sync_text_content_to_arena() const +bool TextNode::sync_text_content_to_arena() const { ensure_text_dependent_cache(); m_enrolled_for_arena_text_content_sync = false; if (m_arena_text_content_in_sync) - return; + return false; auto view = m_text_dependent_cache->text_for_rendering.utf16_view(); - RustFFI::layout_arena_set_text_content( + bool arena_text_content_changed = RustFFI::layout_arena_set_text_content( arena_handle(), slot_id(this), view.has_ascii_storage() ? reinterpret_cast(view.ascii_span().data()) : nullptr, @@ -440,6 +440,7 @@ void TextNode::sync_text_content_to_arena() const text().is_ascii_whitespace(), Unicode::may_require_bidi_processing(view)); m_arena_text_content_in_sync = true; + return arena_text_content_changed; } Utf16String TextNode::compute_text_for_rendering(TextForRenderingCacheKey const& cache_key) const diff --git a/Libraries/LibWeb/Layout/TextNode.h b/Libraries/LibWeb/Layout/TextNode.h index 0b9c75e316eeb..10e5f39a2fc18 100644 --- a/Libraries/LibWeb/Layout/TextNode.h +++ b/Libraries/LibWeb/Layout/TextNode.h @@ -42,7 +42,7 @@ class TextNode : public Node { void invalidate_text_for_rendering(); void enroll_for_arena_text_content_sync() const; - void sync_text_content_to_arena() const; + bool sync_text_content_to_arena() const; Unicode::Segmenter& grapheme_segmenter() const; diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index e1c67f95738c9..dbccd68a122af 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -689,13 +689,26 @@ impl LayoutNodeArena { text: Vec, untransformed_text_is_ascii_whitespace: bool, may_require_bidi_processing: bool, - ) { + ) -> bool { self.assert_owner_thread(); self.data(id); let index = id.slot_index() as usize; if self.text_contents.len() <= index { self.text_contents.resize_with(index + 1, TextContentSlot::default); } + let previous = &self.text_contents[index]; + let changed = previous.generation != id.generation() + || match &previous.content { + Some(content) => { + content.text != text + || content.untransformed_text_is_ascii_whitespace != untransformed_text_is_ascii_whitespace + || content.may_require_bidi_processing != may_require_bidi_processing + } + None => true, + }; + if !changed { + return false; + } self.text_contents[index] = TextContentSlot { generation: id.generation(), content: Some(Box::new(TextContent { @@ -707,6 +720,7 @@ impl LayoutNodeArena { if let Some(slot) = self.text_chunk_caches.get_mut().get_mut(index) { *slot = TextChunkCacheSlot::default(); } + true } pub(crate) fn set_replaced_content_facts(&mut self, id: NodeSlotId, facts: FfiReplacedContentFacts) -> bool { @@ -956,7 +970,7 @@ pub unsafe extern "C" fn layout_arena_set_text_content( length_in_code_units: usize, untransformed_text_is_ascii_whitespace: bool, may_require_bidi_processing: bool, -) { +) -> bool { abort_on_panic(|| { assert!(!arena.is_null(), "layout node arena handle is null"); let text = if length_in_code_units == 0 { @@ -981,8 +995,8 @@ pub unsafe extern "C" fn layout_arena_set_text_content( text, untransformed_text_is_ascii_whitespace, may_require_bidi_processing, - ); - }); + ) + }) } #[unsafe(no_mangle)] From 6d15e152b4707f008562042509a2751dfdd15bf0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:07:41 +0200 Subject: [PATCH 10/14] LibGfx: Add a retained-font handle over the ref/unref FFI Cached layout line data borrows raw Gfx::Font pointers, and a paint-only style change can drop the owning computed values without any layout invalidation noticing. Cross-pass cache entries therefore need to retain every font their glyph runs reference. Add a ref/unref FFI pair and a RetainedFont handle mirroring RetainedFontCascadeList, releasing on drop. --- Libraries/LibGfx/Font/Font.cpp | 14 ++++++ Libraries/LibGfx/Rust/src/font.rs | 75 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Libraries/LibGfx/Font/Font.cpp b/Libraries/LibGfx/Font/Font.cpp index 8e9ed26fdce27..4247a5d006b75 100644 --- a/Libraries/LibGfx/Font/Font.cpp +++ b/Libraries/LibGfx/Font/Font.cpp @@ -31,6 +31,8 @@ float ladybird_gfx_font_glyph_width(void const*, u32); u32 ladybird_gfx_font_glyph_id(void const*, u32); bool ladybird_gfx_font_contains_glyph(void const*, u32); bool ladybird_gfx_font_is_emoji_font(void const*); +void ladybird_gfx_font_ref(void const*); +void ladybird_gfx_font_unref(void const*); } namespace Gfx { @@ -274,3 +276,15 @@ extern "C" bool ladybird_gfx_font_is_emoji_font(void const* font) VERIFY(font); return static_cast(font)->is_emoji_font(); } + +extern "C" void ladybird_gfx_font_ref(void const* font) +{ + VERIFY(font); + static_cast(font)->ref(); +} + +extern "C" void ladybird_gfx_font_unref(void const* font) +{ + VERIFY(font); + static_cast(font)->unref(); +} diff --git a/Libraries/LibGfx/Rust/src/font.rs b/Libraries/LibGfx/Rust/src/font.rs index 255d369332e26..0f916f924fc72 100644 --- a/Libraries/LibGfx/Rust/src/font.rs +++ b/Libraries/LibGfx/Rust/src/font.rs @@ -21,6 +21,8 @@ unsafe extern "C" { forced_presentation: bool, ) -> *const c_void; fn ladybird_gfx_font_cascade_list_first(list: *const c_void) -> *const c_void; + fn ladybird_gfx_font_ref(font: *const c_void); + fn ladybird_gfx_font_unref(font: *const c_void); fn ladybird_gfx_font_cascade_list_ref(list: *const c_void); fn ladybird_gfx_font_cascade_list_unref(list: *const c_void); fn ladybird_gfx_emoji_presentation_for_code_point( @@ -163,33 +165,54 @@ impl<'a> FontCascadeListRef<'a> { } } -/// A strong reference to a `Gfx::FontCascadeList`, keeping the list and every -/// font it can resolve alive until dropped. -pub struct RetainedFontCascadeList { - raw: NonNull, -} +/// Generates a strong-reference handle over a C++ ref/unref FFI pair: +/// retain-on-construct, release-on-drop. +macro_rules! retained_ffi_handle { + ($(#[$documentation:meta])* $name:ident, $ref_function:ident, $unref_function:ident, $type_name:literal) => { + $(#[$documentation])* + pub struct $name { + raw: NonNull, + } -impl RetainedFontCascadeList { - /// # Safety - /// - /// `raw` must point to a live `Gfx::FontCascadeList` at the time of the - /// call. - pub unsafe fn retain(raw: *const c_void) -> Self { - let raw = NonNull::new(raw.cast_mut()).expect("Gfx::FontCascadeList pointer must not be null"); - // SAFETY: The caller guarantees the list is live, and ref() keeps it - // that way until this reference drops. - unsafe { ladybird_gfx_font_cascade_list_ref(raw.as_ptr()) }; - Self { raw } - } + impl $name { + /// # Safety + /// + /// `raw` must point to a live object at the time of the call. + pub unsafe fn retain(raw: *const c_void) -> Self { + let raw = NonNull::new(raw.cast_mut()).expect(concat!($type_name, " pointer must not be null")); + // SAFETY: The caller guarantees the object is live, and the + // reference taken here keeps it that way until drop. + unsafe { $ref_function(raw.as_ptr()) }; + Self { raw } + } + + pub fn as_raw(&self) -> *const c_void { + self.raw.as_ptr() + } + } - pub fn as_raw(&self) -> *const c_void { - self.raw.as_ptr() - } + impl Drop for $name { + fn drop(&mut self) { + // SAFETY: retain() took a strong reference on construction. + unsafe { $unref_function(self.raw.as_ptr()) }; + } + } + }; } -impl Drop for RetainedFontCascadeList { - fn drop(&mut self) { - // SAFETY: retain() took a strong reference on construction. - unsafe { ladybird_gfx_font_cascade_list_unref(self.raw.as_ptr()) }; - } -} +retained_ffi_handle!( + /// A strong reference to a single `Gfx::Font`, keeping it alive until dropped. + RetainedFont, + ladybird_gfx_font_ref, + ladybird_gfx_font_unref, + "Gfx::Font" +); + +retained_ffi_handle!( + /// A strong reference to a `Gfx::FontCascadeList`, keeping the list and every + /// font it can resolve alive until dropped. + RetainedFontCascadeList, + ladybird_gfx_font_cascade_list_ref, + ladybird_gfx_font_cascade_list_unref, + "Gfx::FontCascadeList" +); From 2968d3ffbeb95d50e35631967a278495c2ccac89 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:29:31 +0200 Subject: [PATCH 11/14] LibWeb: Compute SVG transforms from run records alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit computed_transforms fell back to reading the previous pass's committed paintable when the current run's record had no transforms yet, a port artifact from the C++ engine. That made fragment payloads pass-dependent: only the first pass's fragments carried the transforms and every later pass leaned on the committed paintable retaining them — and the readback cannot even distinguish a never-computed value from identity. The run cache's shadow oracle caught the asymmetry as cached-versus-fresh divergences on two SVG tests. Every context already writes a viewport's transforms into the record before entering it and the outermost root's correct default is identity (its own CSS transform applies at paint level), so the record is the whole truth: drop the fallback and consult only the run's own record. Fragments now carry the same transforms no matter which pass produced them. --- .../Rust/src/layout/svg_formatting_context.rs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs index 2f2ce667f8807..010def9b7a77e 100644 --- a/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/svg_formatting_context.rs @@ -494,25 +494,10 @@ impl SvgFormattingContext { } fn computed_transforms(&self, node: Node) -> Option { - if let Some(transforms) = self - .used_values(node) + self.used_values(node) .rare_data .get() .and_then(|cell| cell.borrow().computed_svg_transforms) - { - return Some(transforms); - } - let mut transforms = FfiSvgComputedTransforms::default(); - // SAFETY: `transforms` is writable POD storage and the callback reads - // only paintable geometry retained by the C++ layout node. - let has_transforms = unsafe { - (self.callbacks.read_paintable_svg_transforms)( - self.callbacks.context, - self.callbacks.shell(node), - &raw mut transforms, - ) - }; - has_transforms.then_some(transforms) } fn set_computed_transforms(&self, node: Node, transforms: FfiSvgComputedTransforms) { From 563bf02ff2d1d8daa6bcffe6037b44b7298d14cd Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:29:50 +0200 Subject: [PATCH 12/14] LibWeb: Cache formatting-context run results behind an opt-in flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full layout passes re-run every formatting context from the viewport down even when nothing in a subtree changed. Cache completed runs and replay them: run_formatting_context probes before executing, a hit clones the stored RunOutputs and hands it straight to absorb_run_outputs, and the entire subtree's layout work — nested runs included — never happens. Fragment offsets are containing-block relative and escape payloads carry coordinate-space tags, so a cached tree replays at any position; line data and the owned layout payloads ride shared pointers, so store and hit are refcount bumps. The key is (formatting context type, LayoutInput, the pre-run root cell state the dispatch seam already captures), all plain values, so everything a parent hands a spawned run is keyed. Entries live on the node arena, one per slot, validated by slot generation, the fragment cache epoch, and the viewport size — captured once at probe time and reused at store time, so an invalidation landing mid-run produces a fail-safe miss instead of a forever-valid entry. The C++ bridge stamps the viewport once per pass entry, with full layout, subtree relayout, and saved-abspos replay all funneling through one begin-pass helper, so an entry point cannot forget the stamp. Entries retain every font their glyph runs reference; the raw text pointers in cached line data stay unretained because nothing on the replay path dereferences them. A sweep at the end of every pass entry drops entries whose validity already failed, so invalidated entries whose box never probes again do not accumulate for the document's lifetime. The C++ epoch walks latch the cache mode over the FFI, keeping a single parser of the environment variable. Measurement runs, pass entries, internal context types, devtools collection, float roots (admitted separately once the shadow oracle verifies their replay equivalence corpus-wide), subgridded grid items (their runs copy the parent grid's mid-run track state, which the key cannot see — the probe shares the grid code's subgrid predicate as a conservative superset), and roots whose insets use anchor functions (the structural invariants behind that bypass are recorded at the probe) all bypass. --- Libraries/LibWeb/Layout/LayoutRustBridge.cpp | 15 + Libraries/LibWeb/Layout/Node.cpp | 4 +- .../LibWeb/Rust/src/layout/fc_run_cache.rs | 491 ++++++++++++++++++ .../Rust/src/layout/formatting_context.rs | 20 + .../src/layout/grid_formatting_context.rs | 8 + .../Rust/src/layout/layout_node_arena.rs | 47 ++ Libraries/LibWeb/Rust/src/layout/mod.rs | 1 + .../LibWeb/Rust/src/layout/used_values.rs | 2 +- 8 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 8bff24a1cd4e3..9ca4adf1ba3b3 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -648,6 +648,18 @@ LayoutRustBridge::LayoutRustBridge() = default; LayoutRustBridge::~LayoutRustBridge() = default; +// Stamps the store once per pass entry, so cache entries validate against the +// viewport the pass actually laid out with; a new bridge entry method must call +// this before entering Rust. +static void note_viewport_size_for_pass(Box& pass_root) +{ + auto viewport_rect = pass_root.document().viewport_rect(); + RustFFI::layout_arena_note_viewport_size( + pass_root.arena_handle(), + viewport_rect.width().raw_value(), + viewport_rect.height().raw_value()); +} + void LayoutRustBridge::run_root_layout(Box& viewport, CSSPixels viewport_inline_size, CSSPixels viewport_block_size, bool should_collect_devtools_layout_data) { VERIFY(!m_commit_root); @@ -657,6 +669,7 @@ void LayoutRustBridge::run_root_layout(Box& viewport, CSSPixels viewport_inline_ }; viewport.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(viewport); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); { @@ -681,6 +694,7 @@ void LayoutRustBridge::compute_subtree_layout(Box& root, Painting::Paintable& pa }; root.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(root); auto viewport_rect = root.document().viewport_rect(); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); @@ -707,6 +721,7 @@ void LayoutRustBridge::replay_saved_abspos_layout(Box& box, Painting::Paintable& }; box.document().invalidate_stacking_context_tree(); + note_viewport_size_for_pass(box); auto callbacks = formatting_context_callbacks(); auto sink = commit_sink(); { diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index f13c2c7449ebb..3a14920a31ac1 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -116,7 +116,9 @@ void Node::enroll_for_arena_replaced_content_facts_sync_if_eligible() bool Node::fragment_cache_epochs_enabled() { - static bool const enabled = getenv("LADYBIRD_FC_RUN_CACHE") != nullptr; + // The Rust cache module owns the only LADYBIRD_FC_RUN_CACHE parser; the + // bump walks follow whatever mode it resolved. + static bool const enabled = RustFFI::layout_fc_run_cache_epochs_enabled(); return enabled; } diff --git a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs new file mode 100644 index 0000000000000..f2effa945282d --- /dev/null +++ b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FcRunCacheMode { + Disabled, + Enabled, + /// Hits do not replay: the real layout runs and the entry is verified + /// against it, panicking on any divergence. + Shadow, +} + +fn fc_run_cache_mode_from_environment() -> FcRunCacheMode { + static MODE: std::sync::OnceLock = std::sync::OnceLock::new(); + *MODE.get_or_init(|| match std::env::var("LADYBIRD_FC_RUN_CACHE").as_deref() { + Ok("0") => FcRunCacheMode::Disabled, + Ok("1") => FcRunCacheMode::Enabled, + Ok("shadow") => FcRunCacheMode::Shadow, + Ok(unknown) => { + eprintln!("Unknown LADYBIRD_FC_RUN_CACHE value {unknown:?} (expected 0, 1, or shadow); disabling the run cache"); + FcRunCacheMode::Disabled + } + Err(_) => FcRunCacheMode::Enabled, + }) +} + +/// The complete identity of a memoizable run: the layout input plus the +/// pre-run root record state the dispatch seam captures anyway, so every +/// value a parent hands a spawned run is part of the key. +#[derive(Clone, Copy, PartialEq)] +struct FcRunCacheKey { + fc_type: FfiFormattingContextType, + input: LayoutInput, + root_cells: UsedValuesCellState, +} + +/// What must still be true for a stored entry to be replayed: the slot +/// holds the same box (generation), nothing in its subtree was invalidated +/// (the fragment cache epoch, whose bump walk has no propagation +/// boundary), and the viewport is unchanged (viewport-relative styles do +/// not necessarily funnel through per-node invalidation). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct FcRunCacheValidity { + pub(crate) slot_generation: u8, + pub(crate) fragment_cache_epoch: u32, + pub(crate) viewport: (i32, i32), +} + +struct FcRunCacheEntry { + key: FcRunCacheKey, + validity: FcRunCacheValidity, + outputs: RunOutputs, + /// Keeps every font referenced by cached line data alive: glyph runs + /// borrow raw font pointers, and a paint-only style change can drop + /// the owning computed values without touching any layout epoch. + /// + /// Cached line data also holds raw text_utf16 pointers into arena text + /// slots, deliberately unretained: replay never re-runs line building, + /// commit emits offsets rather than the pointers, and every text change + /// bumps epochs before the next probe, so nothing on the replay path + /// dereferences them. Any future consumer of cached fragment text must + /// snapshot the text alongside the fonts first. + retained_fonts: Vec, +} + +/// Per-document store of completed run results, one entry per slot, +/// surviving across layout passes on the node arena. +#[derive(Default)] +pub(crate) struct FcRunCacheArenaStore { + viewport: Cell<(i32, i32)>, + entries: RefCell>>>, +} + +impl FcRunCacheArenaStore { + pub(crate) fn note_viewport_size(&self, inline_size_raw: i32, block_size_raw: i32) { + self.viewport.set((inline_size_raw, block_size_raw)); + } + + pub(crate) fn viewport_size(&self) -> (i32, i32) { + self.viewport.get() + } + + pub(crate) fn remove_entry(&self, slot: u32) { + if let Some(entry) = self.entries.borrow_mut().get_mut(slot as usize) { + *entry = None; + } + } + + /// A matching entry stays stored — hits hand out shared handles — while + /// a stale entry is evicted on sight, releasing its tree and fonts. + fn matching(&self, slot: u32, validity: FcRunCacheValidity, key: &FcRunCacheKey) -> Option> { + let mut entries = self.entries.borrow_mut(); + let stored = entries.get_mut(slot as usize)?; + let entry = stored.as_ref()?; + if entry.validity != validity { + *stored = None; + return None; + } + if entry.key != *key { + return None; + } + Some(entry.clone()) + } + + fn store(&self, slot: u32, entry: std::rc::Rc) { + let mut entries = self.entries.borrow_mut(); + if entries.len() <= slot as usize { + entries.resize_with(slot as usize + 1, || None); + } + entries[slot as usize] = Some(entry); + } + + /// Drops every entry the keep predicate rejects, releasing its tree and + /// fonts. The end-of-pass sweep uses this so entries invalidated while + /// their box never probes again do not accumulate for the document's + /// lifetime. + pub(crate) fn retain_entries(&self, mut keep: impl FnMut(u32, FcRunCacheValidity) -> bool) { + let mut entries = self.entries.borrow_mut(); + for (slot, stored) in entries.iter_mut().enumerate() { + if let Some(entry) = stored + && !keep(slot as u32, entry.validity) + { + *stored = None; + } + } + } +} + +fn collect_line_data_fonts(line_data: &LineData, fonts: &mut Vec<*const c_void>) { + for line in &line_data.line_boxes { + for fragment in &line.fragments { + if let Some(glyphs) = &fragment.glyphs + && !glyphs.font.is_null() + { + fonts.push(glyphs.font); + } + } + } +} + +fn collect_fragment_tree_fonts(links: &[FragmentLink], fonts: &mut Vec<*const c_void>) { + for link in links { + if let Some(line_data) = &link.fragment.line_data { + collect_line_data_fonts(line_data, fonts); + } + collect_fragment_tree_fonts(&link.fragment.children, fonts); + } +} + +fn run_root_validity(callbacks: &FfiLayoutFcCallbacks, box_: Node) -> FcRunCacheValidity { + let data = NodeFacts::new(callbacks, box_).data(); + FcRunCacheValidity { + slot_generation: data.slot_generation, + fragment_cache_epoch: data.fragment_cache_epoch, + viewport: callbacks.arena().fc_run_cache_store().viewport.get(), + } +} + +/// A run's cache interaction, decided at probe time and concluded after +/// the run executes. Uncacheable classes bypass entirely: measurement and +/// intrinsic-sizing runs produce no fragments; subgridded grid items copy +/// the parent grid's mid-run track state, which the key cannot see; +/// anchor()-positioned roots keep their bypass while the resolved-inset +/// side effects are audited; pass entries and internal runs are not +/// spawned child runs; devtools collection emits per-run callbacks a +/// replay would skip. +enum FcRunCacheAttempt { + Bypass, + Store { + key: Box, + /// Captured at probe time and reused at store time: an invalidation + /// landing between probe and store makes the stored entry look stale + /// on its next probe (a fail-safe miss) instead of being baked into + /// a forever-valid entry. + validity: FcRunCacheValidity, + shadow_entry: Option>, + }, +} + +impl FcRunCacheAttempt { + /// Err carries the entry the caller must replay instead of running. + #[expect(clippy::too_many_arguments)] + fn probe( + purpose: LayoutPurpose, + box_: Node, + parent_grid_is_present: bool, + fc_type: FfiFormattingContextType, + layout_mode: LayoutMode, + should_collect_devtools_layout_data: bool, + callbacks: &FfiLayoutFcCallbacks, + input: &LayoutInput, + root_cells: &UsedValuesCellState, + ) -> Result> { + let mode = fc_run_cache_mode_from_environment(); + if mode == FcRunCacheMode::Disabled + || layout_mode != LayoutMode::Normal + || purpose.is_measurement() + || should_collect_devtools_layout_data + || input.participation == ParticipationInParentFormattingContext::Root + || matches!( + fc_type, + FfiFormattingContextType::InternalReplaced | FfiFormattingContextType::InternalDummy + ) + { + return Ok(Self::Bypass); + } + if fc_type == FfiFormattingContextType::Grid + && parent_grid_is_present + && grid_template_declares_a_subgrid_axis(callbacks, box_) + { + return Ok(Self::Bypass); + } + // Structural invariants behind this root-flag check, to re-verify if it is ever + // narrowed: anchor() applies only to absolutely positioned boxes and every abspos + // box is the root of its own spawned run, so the flag on the run root covers every + // anchor consumer; anchor eligibility is resolved from used values of the same + // pass under the C++ anchor_lookup containing-block guard, never from a previous + // pass; scroll compensation compares chains that converge at the run root and + // registers scroll-shift side effects a replay would skip; and anchor-size() + // resolves to None today, so inset properties are the only anchor dependency a + // run can have. + if has_flag(NodeFacts::new(callbacks, box_).data(), NodeFlag::InsetsUseAnchorFunctions) { + return Ok(Self::Bypass); + } + let key = Box::new(FcRunCacheKey { + fc_type, + input: *input, + root_cells: *root_cells, + }); + let store = callbacks.arena().fc_run_cache_store(); + let validity = run_root_validity(callbacks, box_); + match store.matching(box_.slot_index(), validity, &key) { + Some(entry) if mode == FcRunCacheMode::Shadow => Ok(Self::Store { + key, + validity, + shadow_entry: Some(entry), + }), + Some(entry) => Err(entry), + None => Ok(Self::Store { + key, + validity, + shadow_entry: None, + }), + } + } + + fn conclude(self, callbacks: &FfiLayoutFcCallbacks, box_: Node, outputs: &RunOutputs) { + let Self::Store { + key, + validity, + shadow_entry, + } = self + else { + return; + }; + let Some(root) = &outputs.root else { + return; + }; + let mut fonts = Vec::new(); + if let Some(line_data) = &outputs.root_outcome.line_data { + collect_line_data_fonts(line_data, &mut fonts); + } + collect_fragment_tree_fonts(&root.scoped_descendants, &mut fonts); + fonts.sort_unstable(); + fonts.dedup(); + let entry = FcRunCacheEntry { + key: *key, + validity, + outputs: outputs.clone(), + retained_fonts: fonts + .into_iter() + // SAFETY: every collected font pointer is live during the + // pass that produced the line data now being cached. + .map(|font| unsafe { libgfx_rust::font::RetainedFont::retain(font) }) + .collect(), + }; + if let Some(cached) = shadow_entry { + verify_cached_entry_against_fresh_run(box_.slot_index(), &cached, &entry); + } + callbacks + .arena() + .fc_run_cache_store() + .store(box_.slot_index(), std::rc::Rc::new(entry)); + } +} + +fn verify_cached_entry_against_fresh_run(root_slot: u32, cached: &FcRunCacheEntry, fresh: &FcRunCacheEntry) { + assert!( + cached.outputs.result == fresh.outputs.result, + "run cache shadow: child layout result diverged for slot {root_slot}" + ); + assert!( + cached.outputs.root_outcome.cells == fresh.outputs.root_outcome.cells, + "run cache shadow: body-end root record diverged for slot {root_slot}\ncached: {:?}\nfresh: {:?}", + cached.outputs.root_outcome.cells, + fresh.outputs.root_outcome.cells, + ); + assert!( + cached.outputs.root_outcome.own_metrics_sealed == fresh.outputs.root_outcome.own_metrics_sealed, + "run cache shadow: root seal state diverged for slot {root_slot}" + ); + let cached_fonts: Vec<_> = cached.retained_fonts.iter().map(|font| font.as_raw()).collect(); + let fresh_fonts: Vec<_> = fresh.retained_fonts.iter().map(|font| font.as_raw()).collect(); + assert!( + cached_fonts == fresh_fonts, + "run cache shadow: referenced fonts diverged for slot {root_slot}" + ); + assert_line_data_matches( + root_slot, + cached.outputs.root_outcome.line_data.as_deref(), + fresh.outputs.root_outcome.line_data.as_deref(), + ); + assert_rare_data_matches( + root_slot, + cached.outputs.root_outcome.rare.as_ref(), + fresh.outputs.root_outcome.rare.as_ref(), + ); + assert_unplaced_roots_match(root_slot, cached.outputs.root.as_ref(), fresh.outputs.root.as_ref()); +} + +/// The comparable view of the payloads both `UsedValuesRareData` and +/// `Fragment` carry under identical field names, so the oracle's two +/// comparison sites cannot drift apart when a payload is added. Shared +/// payloads compare by content through the Rc (paths through their +/// process-unique identity first). +macro_rules! shadow_comparable_rare_payloads { + ($carrier:expr) => { + ( + $carrier.table_cell_coordinates, + $carrier.override_borders_data, + $carrier.computed_svg_transforms, + $carrier.svg_viewport_size, + &$carrier.computed_svg_path, + &$carrier.grid_layout_data, + &$carrier.flex_layout_data, + &$carrier.used_grid_tracks, + ) + }; +} + +fn line_data_matches(cached: Option<&LineData>, fresh: Option<&LineData>) -> bool { + cached == fresh +} + +fn assert_line_data_matches(root_slot: u32, cached: Option<&LineData>, fresh: Option<&LineData>) { + assert!( + line_data_matches(cached, fresh), + "run cache shadow: root line data diverged for slot {root_slot}" + ); +} + +fn assert_rare_data_matches(root_slot: u32, cached: Option<&UsedValuesRareData>, fresh: Option<&UsedValuesRareData>) { + assert!( + cached.is_some() == fresh.is_some(), + "run cache shadow: root rare data presence diverged for slot {root_slot}" + ); + let (Some(cached), Some(fresh)) = (cached, fresh) else { + return; + }; + assert!( + shadow_comparable_rare_payloads!(cached) == shadow_comparable_rare_payloads!(fresh) + && cached.abspos_layout_inputs == fresh.abspos_layout_inputs, + "run cache shadow: root rare data diverged for slot {root_slot}" + ); +} + +fn sorted_by_key(items: &[T], key: impl Fn(&T) -> K) -> Vec { + let mut sorted = items.to_vec(); + sorted.sort_by_key(|item| key(item)); + sorted +} + +fn assert_unplaced_roots_match(root_slot: u32, cached: Option<&UnplacedRootFragment>, fresh: Option<&UnplacedRootFragment>) { + assert!( + cached.is_some() == fresh.is_some(), + "run cache shadow: unplaced root presence diverged for slot {root_slot}" + ); + let (Some(cached), Some(fresh)) = (cached, fresh) else { + return; + }; + assert!( + cached.node == fresh.node, + "run cache shadow: unplaced root node diverged for slot {root_slot}" + ); + + // Escape lists are collected partly from hash-map sweeps, whose order + // is not deterministic between runs; consumption sorts registrations + // into tree order, so the oracle compares them order-insensitively. + let pending_order = + |child: &PendingAbsposChild| (child.child_box.slot_index(), child.coordinate_space_box.slot_index()); + let cached_pending = sorted_by_key(&cached.propagated_pending_abspos, pending_order); + let fresh_pending = sorted_by_key(&fresh.propagated_pending_abspos, pending_order); + assert!( + cached_pending == fresh_pending, + "run cache shadow: propagated abspos children diverged for slot {root_slot}\ncached: {cached_pending:#?}\nfresh: {fresh_pending:#?}" + ); + let candidate_order = + |candidate: &AnchorCandidate| (candidate.node.slot_index(), candidate.coordinate_space_box.slot_index()); + let cached_candidates = sorted_by_key(&cached.propagated_anchor_candidates, candidate_order); + let fresh_candidates = sorted_by_key(&fresh.propagated_anchor_candidates, candidate_order); + assert!( + cached_candidates == fresh_candidates, + "run cache shadow: propagated anchor candidates diverged for slot {root_slot}" + ); + let inline_rect_order = + |rect: &InlineContainingBlockRect| (rect.inline_box.slot_index(), rect.coordinate_space_box.slot_index()); + let cached_inline_rects = sorted_by_key(&cached.propagated_inline_containing_block_rects, inline_rect_order); + let fresh_inline_rects = sorted_by_key(&fresh.propagated_inline_containing_block_rects, inline_rect_order); + assert!( + cached_inline_rects == fresh_inline_rects, + "run cache shadow: propagated inline containing block rects diverged for slot {root_slot}" + ); + let contribution_order = |contribution: &AbsposContainingBlockInfoContribution| contribution.child_box.slot_index(); + let cached_contributions = sorted_by_key(&cached.propagated_abspos_containing_block_info, contribution_order); + let fresh_contributions = sorted_by_key(&fresh.propagated_abspos_containing_block_info, contribution_order); + assert!( + cached_contributions == fresh_contributions, + "run cache shadow: propagated containing block info diverged for slot {root_slot}" + ); + + assert_link_lists_match(root_slot, &cached.scoped_descendants, &fresh.scoped_descendants); +} + +fn assert_link_lists_match(root_slot: u32, cached: &[FragmentLink], fresh: &[FragmentLink]) { + assert!( + cached.len() == fresh.len(), + "run cache shadow: fragment child count diverged for slot {root_slot}" + ); + for (cached_link, fresh_link) in cached.iter().zip(fresh) { + assert_links_match(root_slot, cached_link, fresh_link); + } +} + +fn assert_links_match(root_slot: u32, cached: &FragmentLink, fresh: &FragmentLink) { + let mut diverged = Vec::new(); + if cached.committed_offset != fresh.committed_offset { + diverged.push("committed_offset"); + } + if (cached.inset_left, cached.inset_right, cached.inset_top, cached.inset_bottom) + != (fresh.inset_left, fresh.inset_right, fresh.inset_top, fresh.inset_bottom) + { + diverged.push("insets"); + } + if cached.containing_line_box_index != fresh.containing_line_box_index { + diverged.push("containing_line_box_index"); + } + if cached.abspos_layout_inputs != fresh.abspos_layout_inputs { + diverged.push("abspos_layout_inputs"); + } + collect_diverged_fragment_fields(&cached.fragment, &fresh.fragment, &mut diverged); + assert!( + diverged.is_empty(), + "run cache shadow: fragment for slot {} diverged under run root slot {root_slot}: {}", + fresh.fragment.node.slot_index(), + diverged.join(", ") + ); + assert_link_lists_match(root_slot, &cached.fragment.children, &fresh.fragment.children); +} + +fn collect_diverged_fragment_fields(cached: &Fragment, fresh: &Fragment, diverged: &mut Vec<&'static str>) { + if cached.node != fresh.node { + diverged.push("node"); + } + if (cached.content_inline_size, cached.content_block_size) != (fresh.content_inline_size, fresh.content_block_size) { + diverged.push("content_size"); + } + if (cached.margin_left, cached.margin_right, cached.margin_top, cached.margin_bottom) + != (fresh.margin_left, fresh.margin_right, fresh.margin_top, fresh.margin_bottom) + { + diverged.push("margins"); + } + if (cached.border_left, cached.border_right, cached.border_top, cached.border_bottom) + != (fresh.border_left, fresh.border_right, fresh.border_top, fresh.border_bottom) + { + diverged.push("borders"); + } + if (cached.padding_left, cached.padding_right, cached.padding_top, cached.padding_bottom) + != (fresh.padding_left, fresh.padding_right, fresh.padding_top, fresh.padding_bottom) + { + diverged.push("paddings"); + } + if shadow_comparable_rare_payloads!(cached) != shadow_comparable_rare_payloads!(fresh) { + diverged.push("rare payloads"); + } + if !line_data_matches(cached.line_data.as_deref(), fresh.line_data.as_deref()) { + diverged.push("line_data"); + } +} diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 199fb99c13f18..251c60aa4a412 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -785,6 +785,7 @@ pub(crate) struct ChildLayoutResult { pub table_box_in_wrapper_border_box_block_size: Option, } +#[derive(Clone)] pub(crate) struct RunRootOutcome { cells: UsedValuesCellState, own_metrics_sealed: bool, @@ -807,6 +808,7 @@ impl RunRootOutcome { } } +#[derive(Clone)] pub(crate) struct RunOutputs { pub(crate) result: ChildLayoutResult, pub(crate) root: Option, @@ -1619,6 +1621,20 @@ fn run_formatting_context( parent_block: Option<&BlockFormattingContext>, ) -> ChildLayoutResult { let root_cells = UsedValuesCellState::capture(parent_used); + let cache_attempt = match FcRunCacheAttempt::probe( + purpose, + box_, + parent_grid.is_some(), + fc_type, + layout_mode, + should_collect_devtools_layout_data, + &callbacks, + &input, + &root_cells, + ) { + Ok(attempt) => attempt, + Err(entry) => return absorb_run_outputs(parent_fragments, parent_used, box_, entry.outputs.clone()), + }; let outputs = execute_formatting_context_run( purpose, root_cells, @@ -1631,6 +1647,7 @@ fn run_formatting_context( input, parent_block, ); + cache_attempt.conclude(&callbacks, box_, &outputs); absorb_run_outputs(parent_fragments, parent_used, box_, outputs) } @@ -2098,6 +2115,7 @@ pub unsafe extern "C" fn rust_layout_run_root_layout( std::ptr::null_mut(), sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } @@ -2219,6 +2237,7 @@ pub unsafe extern "C" fn rust_layout_compute_subtree_layout( paintable_to_replace, sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } @@ -2265,5 +2284,6 @@ pub unsafe extern "C" fn rust_layout_replay_saved_abspos_layout( paintable_to_replace, sink, ); + callbacks.arena().sweep_stale_fc_run_cache_entries(); }); } diff --git a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs index 885508c63c952..659b94712fc08 100644 --- a/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/grid_formatting_context.rs @@ -1130,6 +1130,14 @@ impl ParentGridData { } } +/// Conservative superset of is_subgridded() for callers outside a live grid run +/// (the fc-run-cache probe): a declared subgrid axis counts regardless of the +/// parent-grid placement check only a run in progress can make. +fn grid_template_declares_a_subgrid_axis(callbacks: &FfiLayoutFcCallbacks, box_: Node) -> bool { + let grid_style = ComputedValuesView::new(&callbacks.style_payloads(box_).groups).grid_values(); + grid_style.template_columns.is_subgrid || grid_style.template_rows.is_subgrid +} + impl GridFormattingContext { pub(crate) fn new(run: &FormattingContextRun, parent_grid: Option<&GridFormattingContext>) -> Self { let grid_container = run.box_; diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index dbccd68a122af..087f9354e8a35 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -229,6 +229,7 @@ pub(crate) struct LayoutNodeArena { raw_table_column_spans: HashMap, run_used_records: RefCell>, next_run_nonce: Cell, + fc_run_cache_store: crate::layout::FcRunCacheArenaStore, owner_thread: thread::ThreadId, } @@ -249,10 +250,34 @@ impl LayoutNodeArena { raw_table_column_spans: HashMap::new(), run_used_records: RefCell::new(Vec::new()), next_run_nonce: Cell::new(1), + fc_run_cache_store: crate::layout::FcRunCacheArenaStore::default(), owner_thread: thread::current().id(), } } + pub(crate) fn fc_run_cache_store(&self) -> &crate::layout::FcRunCacheArenaStore { + &self.fc_run_cache_store + } + + /// Drops entries whose slot, epoch, or viewport stamp no longer match. + /// Runs at the end of every full pass so invalidated entries whose box + /// never probes again do not accumulate for the document's lifetime. + pub(crate) fn sweep_stale_fc_run_cache_entries(&self) { + let viewport = self.fc_run_cache_store.viewport_size(); + self.fc_run_cache_store.retain_entries(|slot, validity| { + let Some(metadata) = self.slot_metadata.get(slot as usize) else { + return false; + }; + if !metadata.occupied || metadata.generation != validity.slot_generation || viewport != validity.viewport { + return false; + } + let id = NodeSlotId::new(slot, metadata.generation); + // SAFETY: The slot is occupied at the matching generation, so the + // pointer addresses a live NodeData. + unsafe { (*self.data(id)).fragment_cache_epoch == validity.fragment_cache_epoch } + }); + } + fn assert_owner_thread(&self) { debug_assert_eq!(self.owner_thread, thread::current().id()); } @@ -356,6 +381,7 @@ impl LayoutNodeArena { ); *slot = RunRecordSlot::default(); } + self.fc_run_cache_store.remove_entry(index); self.raw_table_column_spans.remove(&id); *self.data_mut(index) = NodeData::default(); @@ -961,6 +987,11 @@ pub unsafe extern "C" fn layout_arena_free(arena: *mut c_void, id: NodeSlotId, g }); } +#[unsafe(no_mangle)] +pub extern "C" fn layout_fc_run_cache_epochs_enabled() -> bool { + crate::layout::fc_run_cache_mode_from_environment() != crate::layout::FcRunCacheMode::Disabled +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_text_content( arena: *mut c_void, @@ -1013,6 +1044,22 @@ pub unsafe extern "C" fn layout_arena_set_replaced_content_facts( }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_note_viewport_size( + arena: *mut c_void, + viewport_inline_size_raw: i32, + viewport_block_size_raw: i32, +) { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + // SAFETY: The C++ wrapper keeps the arena alive for this call and + // serializes all access on the document thread. + unsafe { &*arena.cast::() } + .fc_run_cache_store() + .note_viewport_size(viewport_inline_size_raw, viewport_block_size_raw); + }); +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_raw_table_column_span( arena: *mut c_void, diff --git a/Libraries/LibWeb/Rust/src/layout/mod.rs b/Libraries/LibWeb/Rust/src/layout/mod.rs index d575847031124..33a6994665306 100644 --- a/Libraries/LibWeb/Rust/src/layout/mod.rs +++ b/Libraries/LibWeb/Rust/src/layout/mod.rs @@ -36,6 +36,7 @@ include!("text_chunker.rs"); include!("replaced_with_children_formatting_context.rs"); include!("table_formatting_context.rs"); include!("geometry.rs"); +include!("fc_run_cache.rs"); mod layout_node_arena; pub mod node_data; include!("run_records.rs"); diff --git a/Libraries/LibWeb/Rust/src/layout/used_values.rs b/Libraries/LibWeb/Rust/src/layout/used_values.rs index 4fe4fc28e37b9..4492effac4baf 100644 --- a/Libraries/LibWeb/Rust/src/layout/used_values.rs +++ b/Libraries/LibWeb/Rust/src/layout/used_values.rs @@ -120,7 +120,7 @@ pub(crate) struct LineData { pub(crate) inline_box_pieces: Vec, } -#[derive(Default)] +#[derive(Clone, Default)] pub(crate) struct UsedValuesRareData { pub(crate) table_cell_coordinates: Option, pub(crate) computed_svg_path: Option>, From 545989c281af55b1f7dd29fda5676589c3cb49dd Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:34:24 +0200 Subject: [PATCH 13/14] LibWeb: Drop the paintable SVG transforms readback callback Computing SVG transforms from run records alone left the callback with no callers. --- Libraries/LibWeb/Layout/LayoutRustBridge.cpp | 17 ----------------- .../Rust/src/layout/formatting_context.rs | 2 -- 2 files changed, 19 deletions(-) diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 9ca4adf1ba3b3..d11701e22b578 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -1149,23 +1149,6 @@ RustFFI::FfiLayoutFcCallbacks LayoutRustBridge::formatting_context_callbacks() }; } return true; }, - .read_paintable_svg_transforms = [](void*, void* node, RustFFI::FfiSvgComputedTransforms* out) { - VERIFY(out); - auto const* paintable = static_cast(node)->paintable_ptr(); - Painting::SVGGraphicsPaintable::ComputedTransforms const* transforms = nullptr; - if (auto const* svg_graphics_paintable = as_if(paintable)) - transforms = &svg_graphics_paintable->computed_transforms(); - if (auto const* svg_foreign_object_paintable = as_if(paintable)) - transforms = &svg_foreign_object_paintable->computed_transforms(); - if (auto const* svg_svg_paintable = as_if(paintable)) - transforms = &svg_svg_paintable->computed_transforms(); - if (!transforms) - return false; - *out = { - .viewbox_transform = to_ffi_affine_transform(transforms->svg_to_viewbox_transform()), - .svg_transform = to_ffi_affine_transform(transforms->svg_transform()), - }; - return true; }, .compute_svg_path = [](void*, void* node, RustFFI::FfiSvgPathRequest request) { auto const* node_with_style = as_if(*static_cast(node)); VERIFY(node_with_style); diff --git a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs index 251c60aa4a412..37c4866a8dda8 100644 --- a/Libraries/LibWeb/Rust/src/layout/formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/formatting_context.rs @@ -967,8 +967,6 @@ pub struct FfiLayoutFcCallbacks { pub build_svg_facts: unsafe extern "C" fn(*mut c_void, *mut c_void) -> FfiSvgElementFacts, pub read_paintable_geometry: unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut crate::layout::FfiPaintableGeometry) -> bool, - pub read_paintable_svg_transforms: - unsafe extern "C" fn(*mut c_void, *mut c_void, *mut FfiSvgComputedTransforms) -> bool, pub compute_svg_path: unsafe extern "C" fn(*mut c_void, *mut c_void, FfiSvgPathRequest) -> FfiSvgPathResult, pub svg_image_bounding_box: unsafe extern "C" fn(*mut c_void, *mut c_void, CssPixels, CssPixels) -> FfiFloatRect, pub anchor_lookup: unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const *mut c_void, usize) -> NodeSlotId, From d0eda61dd1ffe4f1a7ebd5a4634849c7a0502b6e Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 11 Aug 2026 15:46:01 +0200 Subject: [PATCH 14/14] LibWeb: Add internals.layoutRunCacheHitCount and layout run cache tests The counter mirrors fullLayoutCount/partialLayoutCount and lets tests assert that a layout pass served a subtree from the cache, or that an invalidation correctly defeated it. It counts replayed probes on the arena store and surfaces through NodeArena into internals. The tests cover: a clean sibling subtree reused at a shifted position with deep descendant geometry following, reuse of an inline-block whose auto height resolves through the block formatting context root path, an inline-flex whose baseline alignment relies on cached baselines, scroll offset and scrollable overflow surviving a moved reuse, and two must-miss cases: a mutation inside the subtree and a containing-block width change. Two further tests pin the invalidation funnel: an animated font-size transition over a cacheable inline-block descendant, which replays stale line data without the funnel bump because the animation fast path restyles inheriting descendants with no per-element invalidation, and an inherited font change over a depth-two anonymous table cell, which only the recursive anonymous style propagation reaches. Three more pin the funnel's precision and the language channel: a paint-only background change must leave the subtree's cached run replayable, a filter change must defeat it because filter establishes a fixed-positioning containing block, and a lang change under text-transform: uppercase must defeat it because locale-sensitive casing re-renders the text. --- Libraries/LibWeb/Internals/Internals.cpp | 6 +++ Libraries/LibWeb/Internals/Internals.h | 1 + Libraries/LibWeb/Internals/Internals.idl | 1 + Libraries/LibWeb/Layout/NodeArena.cpp | 5 +++ Libraries/LibWeb/Layout/NodeArena.h | 1 + .../LibWeb/Rust/src/layout/fc_run_cache.rs | 26 ++++++++--- .../Rust/src/layout/layout_node_arena.rs | 12 ++++++ .../layout-run-cache-animated-font-size.txt | 3 ++ ...out-run-cache-anonymous-cell-font-size.txt | 3 ++ ...-run-cache-background-change-preserved.txt | 1 + .../layout-run-cache-filter-change-misses.txt | 1 + .../layout-run-cache-flex-baseline.txt | 3 ++ ...out-run-cache-inline-block-auto-height.txt | 2 + ...ayout-run-cache-inside-mutation-misses.txt | 2 + .../layout-run-cache-lang-change-misses.txt | 1 + .../layout-run-cache-scroll-preserved.txt | 5 +++ .../layout-run-cache-sibling-growth.txt | 3 ++ .../layout-run-cache-width-change-misses.txt | 2 + .../layout-run-cache-animated-font-size.html | 31 +++++++++++++ ...ut-run-cache-anonymous-cell-font-size.html | 26 +++++++++++ ...run-cache-background-change-preserved.html | 32 ++++++++++++++ ...layout-run-cache-filter-change-misses.html | 32 ++++++++++++++ .../input/layout-run-cache-flex-baseline.html | 37 ++++++++++++++++ ...ut-run-cache-inline-block-auto-height.html | 33 ++++++++++++++ ...yout-run-cache-inside-mutation-misses.html | 32 ++++++++++++++ .../layout-run-cache-lang-change-misses.html | 30 +++++++++++++ .../layout-run-cache-scroll-preserved.html | 43 +++++++++++++++++++ .../layout-run-cache-sibling-growth.html | 42 ++++++++++++++++++ .../layout-run-cache-width-change-misses.html | 34 +++++++++++++++ 29 files changed, 444 insertions(+), 6 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt create mode 100644 Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html create mode 100644 Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index cbde43e351696..e319dd2be2e8d 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -644,6 +645,11 @@ WebIDL::UnsignedLongLong Internals::full_layout_count() return window().associated_document().full_layout_count(); } +WebIDL::UnsignedLongLong Internals::layout_run_cache_hit_count() +{ + return window().associated_document().layout_node_arena().formatting_context_run_cache_hit_count(); +} + WebIDL::UnsignedLongLong Internals::accumulated_visual_context_tree_build_count() { auto paintable = window().associated_document().unsafe_paintable(); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index 65c098fa828ac..9a612b90c3f4a 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -110,6 +110,7 @@ class WEB_API Internals final : public InternalsBase { void set_content_blocking_enabled(bool enabled); WebIDL::UnsignedLongLong partial_layout_count(); WebIDL::UnsignedLongLong full_layout_count(); + WebIDL::UnsignedLongLong layout_run_cache_hit_count(); WebIDL::UnsignedLongLong accumulated_visual_context_tree_build_count(); void set_autoplay_policy(Utf16String const& policy); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index a2403ed4ba699..e957bc6fa874e 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -89,6 +89,7 @@ interface Internals { undefined setContentBlockingEnabled(boolean enabled); unsigned long long partialLayoutCount(); unsigned long long fullLayoutCount(); + unsigned long long layoutRunCacheHitCount(); unsigned long long accumulatedVisualContextTreeBuildCount(); undefined setAutoplayPolicy(Utf16DOMString policy); diff --git a/Libraries/LibWeb/Layout/NodeArena.cpp b/Libraries/LibWeb/Layout/NodeArena.cpp index 9732a29463ca6..975afb02d691e 100644 --- a/Libraries/LibWeb/Layout/NodeArena.cpp +++ b/Libraries/LibWeb/Layout/NodeArena.cpp @@ -35,6 +35,11 @@ void NodeArena::free(RustFFI::NodeSlotId slot, u32 generation) RustFFI::layout_arena_free(m_handle, slot, generation); } +u64 NodeArena::formatting_context_run_cache_hit_count() const +{ + return RustFFI::layout_arena_fc_run_cache_hit_count(m_handle); +} + void NodeArena::enroll_text_node_for_content_sync(TextNode const& text_node) { m_text_nodes_enrolled_for_content_sync.append(text_node.make_weak_ptr()); diff --git a/Libraries/LibWeb/Layout/NodeArena.h b/Libraries/LibWeb/Layout/NodeArena.h index 73de506ee82af..bd26ee637271d 100644 --- a/Libraries/LibWeb/Layout/NodeArena.h +++ b/Libraries/LibWeb/Layout/NodeArena.h @@ -35,6 +35,7 @@ class WEB_API NodeArena : public RefCounted { RustFFI::NodeAllocation allocate(); void free(RustFFI::NodeSlotId, u32 generation); void* handle() const { return m_handle; } + u64 formatting_context_run_cache_hit_count() const; void enroll_text_node_for_content_sync(TextNode const&); void enroll_node_for_replaced_content_facts_sync(Node const&); diff --git a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs index f2effa945282d..4d17468577953 100644 --- a/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs +++ b/Libraries/LibWeb/Rust/src/layout/fc_run_cache.rs @@ -71,6 +71,7 @@ struct FcRunCacheEntry { #[derive(Default)] pub(crate) struct FcRunCacheArenaStore { viewport: Cell<(i32, i32)>, + hit_count: Cell, entries: RefCell>>>, } @@ -83,6 +84,10 @@ impl FcRunCacheArenaStore { self.viewport.get() } + pub(crate) fn hit_count(&self) -> u64 { + self.hit_count.get() + } + pub(crate) fn remove_entry(&self, slot: u32) { if let Some(entry) = self.entries.borrow_mut().get_mut(slot as usize) { *entry = None; @@ -233,12 +238,21 @@ impl FcRunCacheAttempt { let store = callbacks.arena().fc_run_cache_store(); let validity = run_root_validity(callbacks, box_); match store.matching(box_.slot_index(), validity, &key) { - Some(entry) if mode == FcRunCacheMode::Shadow => Ok(Self::Store { - key, - validity, - shadow_entry: Some(entry), - }), - Some(entry) => Err(entry), + Some(entry) if mode == FcRunCacheMode::Shadow => { + // A shadow match is the same event a replay would be, so the + // hit counter reports it: the hit-count tests hold under the + // oracle as well. + store.hit_count.set(store.hit_count.get() + 1); + Ok(Self::Store { + key, + validity, + shadow_entry: Some(entry), + }) + } + Some(entry) => { + store.hit_count.set(store.hit_count.get() + 1); + Err(entry) + } None => Ok(Self::Store { key, validity, diff --git a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs index 087f9354e8a35..d25324e90213b 100644 --- a/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs +++ b/Libraries/LibWeb/Rust/src/layout/layout_node_arena.rs @@ -992,6 +992,18 @@ pub extern "C" fn layout_fc_run_cache_epochs_enabled() -> bool { crate::layout::fc_run_cache_mode_from_environment() != crate::layout::FcRunCacheMode::Disabled } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_fc_run_cache_hit_count(arena: *mut c_void) -> u64 { + abort_on_panic(|| { + assert!(!arena.is_null(), "layout node arena handle is null"); + // SAFETY: The C++ wrapper keeps the arena alive for this call and + // serializes all access on the document thread. + unsafe { &*arena.cast::() } + .fc_run_cache_store() + .hit_count() + }) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn layout_arena_set_text_content( arena: *mut c_void, diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt b/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt new file mode 100644 index 0000000000000..995cbb1881694 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-animated-font-size.txt @@ -0,0 +1,3 @@ +grew: true +height before: 24 +height during: 72 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt b/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt new file mode 100644 index 0000000000000..d8dc450c26af4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-anonymous-cell-font-size.txt @@ -0,0 +1,3 @@ +grew: true +height before: 12 +height after: 48 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt b/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt new file mode 100644 index 0000000000000..d0f3fef1ae8f0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-background-change-preserved.txt @@ -0,0 +1 @@ +hit delta after paint-only change: 1 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt new file mode 100644 index 0000000000000..4870161014d4c --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-filter-change-misses.txt @@ -0,0 +1 @@ +hit delta after filter change: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt b/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt new file mode 100644 index 0000000000000..014d0474380f1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-flex-baseline.txt @@ -0,0 +1,3 @@ +hit delta: >=1 +big moved by: 30 +big rect: 9.703125,80,32 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt b/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt new file mode 100644 index 0000000000000..5461dce57e229 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-inline-block-auto-height.txt @@ -0,0 +1,2 @@ +hit delta: 1 +cached rect: 60,150,45 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt new file mode 100644 index 0000000000000..1ba9f5b2406dc --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-inside-mutation-misses.txt @@ -0,0 +1,2 @@ +hit delta: 0 +cached height: 70 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt new file mode 100644 index 0000000000000..6c37ab60981ef --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-lang-change-misses.txt @@ -0,0 +1 @@ +hit delta after lang change: 0 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt b/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt new file mode 100644 index 0000000000000..6b0743e647dce --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-scroll-preserved.txt @@ -0,0 +1,5 @@ +hit delta: 1 +scrollTop: 120 +scrollHeight: 400 +cached top: 90 +scrollTop after set: 300 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt b/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt new file mode 100644 index 0000000000000..2e65a82ce3914 --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-sibling-growth.txt @@ -0,0 +1,3 @@ +hit delta: 1 +deep before: 0,60,60,20 +deep after: 0,90,60,20 diff --git a/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt b/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt new file mode 100644 index 0000000000000..c1aeb66c7532d --- /dev/null +++ b/Tests/LibWeb/Text/expected/layout-run-cache-width-change-misses.txt @@ -0,0 +1,2 @@ +hit delta: 0 +inner width: 100 diff --git a/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html b/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html new file mode 100644 index 0000000000000..75b1aac727c21 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-animated-font-size.html @@ -0,0 +1,31 @@ + + + +
words words words
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html b/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html new file mode 100644 index 0000000000000..a23aa136c9b0e --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-anonymous-cell-font-size.html @@ -0,0 +1,26 @@ + + + +
words words words
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html b/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html new file mode 100644 index 0000000000000..ba017309b3817 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-background-change-preserved.html @@ -0,0 +1,32 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html new file mode 100644 index 0000000000000..585bb685a33cf --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-filter-change-misses.html @@ -0,0 +1,32 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html b/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html new file mode 100644 index 0000000000000..5cd0fedb03888 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-flex-baseline.html @@ -0,0 +1,37 @@ + + + +
+
x
A
b
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html b/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html new file mode 100644 index 0000000000000..7ab4dcfb8fd02 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-inline-block-auto-height.html @@ -0,0 +1,33 @@ + + + +
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html new file mode 100644 index 0000000000000..f6cf452caaf6b --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-inside-mutation-misses.html @@ -0,0 +1,32 @@ + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html new file mode 100644 index 0000000000000..ab829c50c3bb6 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-lang-change-misses.html @@ -0,0 +1,30 @@ + + + +
+
istanbul
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html b/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html new file mode 100644 index 0000000000000..667d037ed8c36 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-scroll-preserved.html @@ -0,0 +1,43 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html b/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html new file mode 100644 index 0000000000000..5914c01169dfa --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-sibling-growth.html @@ -0,0 +1,42 @@ + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html b/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html new file mode 100644 index 0000000000000..c3916463a0ab1 --- /dev/null +++ b/Tests/LibWeb/Text/input/layout-run-cache-width-change-misses.html @@ -0,0 +1,34 @@ + + + +
+
+
+
+
+