1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 14:37:46 +00:00

LibWeb: Optimize scroll offset and clip state recalculation

In this commit we have optimized the handling of scroll offsets and
clip rectangles to improve performance. Previously, the process
involved multiple full traversals of the paintable tree before each
repaint, which was highly inefficient, especially on pages with a
large number of paintables. The steps were:

1. Traverse the paintable tree to identify all boxes with scrollable or
   clipped overflow.
2. Gather the accumulated scroll offset or clip rectangle for each box.
3. Perform another traversal to apply the corresponding scroll offset
   and clip rectangle to each paintable.

To address this, we've adopted a new strategy that separates the
assignment of the scroll/clip frame from the refresh of accumulated
scroll offsets and clip rectangles, thus reducing the workload:

1. Post-relayout: Identify all boxes with overflow and link each
   paintable to the state of its containing scroll/clip frame.
2. Pre-repaint: Update the clip rectangle and scroll offset only in the
   previously identified boxes.

This adjustment ensures that the costly tree traversals are only
necessary after a relayout, substantially decreasing the amount of work
required before each repaint.
This commit is contained in:
Aliaksandr Kalenik 2024-02-08 17:30:07 +01:00 committed by Andreas Kling
parent fc40d35012
commit 76d1536307
8 changed files with 189 additions and 104 deletions

View file

@ -1064,6 +1064,11 @@ void Document::update_layout()
set_needs_to_resolve_paint_only_properties();
if (navigable()->is_traversable()) {
// NOTE: The assignment of scroll frames only needs to occur for traversables because they take care of all
// nested navigable documents.
paintable()->assign_scroll_frames();
paintable()->assign_clip_frames();
page().client().page_did_layout();
}

View file

@ -2108,21 +2108,24 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig
document->update_paint_and_hit_testing_properties_if_needed();
HashMap<Painting::PaintableBox const*, Painting::ViewportPaintable::ScrollFrame> scroll_frames;
auto& viewport_paintable = *document->paintable();
// NOTE: We only need to refresh the scroll state for traversables because they are responsible
// for tracking the state of all nested navigables.
if (is_traversable()) {
document->paintable()->assign_scroll_frame_ids(scroll_frames);
document->paintable()->assign_clip_rectangles();
viewport_paintable.refresh_scroll_state();
viewport_paintable.refresh_clip_state();
}
document->paintable()->paint_all_phases(context);
viewport_paintable.paint_all_phases(context);
// FIXME: Support scrollable frames inside iframes.
if (is_traversable()) {
Vector<Gfx::IntPoint> scroll_offsets_by_frame_id;
scroll_offsets_by_frame_id.resize(scroll_frames.size());
for (auto [_, scrollable_frame] : scroll_frames) {
auto scroll_offset = context.rounded_device_point(scrollable_frame.offset).to_type<int>();
scroll_offsets_by_frame_id[scrollable_frame.id] = scroll_offset;
scroll_offsets_by_frame_id.resize(viewport_paintable.scroll_state.size());
for (auto [_, scrollable_frame] : viewport_paintable.scroll_state) {
auto scroll_offset = context.rounded_device_point(scrollable_frame->offset).to_type<int>();
scroll_offsets_by_frame_id[scrollable_frame->id] = scroll_offset;
}
recording_painter.apply_scroll_offsets(scroll_offsets_by_frame_id);
}

View file

@ -27,23 +27,44 @@ Layout::InlineNode const& InlinePaintable::layout_node() const
return static_cast<Layout::InlineNode const&>(Paintable::layout_node());
}
Optional<int> InlinePaintable::scroll_frame_id() const
{
if (m_enclosing_scroll_frame)
return m_enclosing_scroll_frame->id;
return {};
}
Optional<CSSPixelPoint> InlinePaintable::enclosing_scroll_frame_offset() const
{
if (m_enclosing_scroll_frame)
return m_enclosing_scroll_frame->offset;
return {};
}
Optional<CSSPixelRect> InlinePaintable::clip_rect() const
{
if (m_enclosing_clip_frame)
return m_enclosing_clip_frame->rect;
return {};
}
void InlinePaintable::before_paint(PaintContext& context, PaintPhase) const
{
if (m_scroll_frame_id.has_value()) {
if (scroll_frame_id().has_value()) {
context.recording_painter().save();
context.recording_painter().set_scroll_frame_id(m_scroll_frame_id.value());
context.recording_painter().set_scroll_frame_id(scroll_frame_id().value());
}
if (m_clip_rect.has_value()) {
if (clip_rect().has_value()) {
context.recording_painter().save();
context.recording_painter().add_clip_rect(context.enclosing_device_rect(*m_clip_rect).to_type<int>());
context.recording_painter().add_clip_rect(context.enclosing_device_rect(*clip_rect()).to_type<int>());
}
}
void InlinePaintable::after_paint(PaintContext& context, PaintPhase) const
{
if (m_clip_rect.has_value())
if (clip_rect().has_value())
context.recording_painter().restore();
if (m_scroll_frame_id.has_value())
if (scroll_frame_id().has_value())
context.recording_painter().restore();
}
@ -186,8 +207,8 @@ Optional<HitTestResult> InlinePaintable::hit_test(CSSPixelPoint position, HitTes
return {};
auto position_adjusted_by_scroll_offset = position;
if (m_enclosing_scroll_frame_offset.has_value())
position_adjusted_by_scroll_offset.translate_by(-m_enclosing_scroll_frame_offset.value());
if (enclosing_scroll_frame_offset().has_value())
position_adjusted_by_scroll_offset.translate_by(-enclosing_scroll_frame_offset().value());
for (auto& fragment : m_fragments) {
if (fragment.paintable().stacking_context())

View file

@ -37,9 +37,12 @@ public:
void set_box_shadow_data(Vector<ShadowData>&& box_shadow_data) { m_box_shadow_data = move(box_shadow_data); }
Vector<ShadowData> const& box_shadow_data() const { return m_box_shadow_data; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_enclosing_scroll_frame_offset(CSSPixelPoint offset) { m_enclosing_scroll_frame_offset = offset; }
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
void set_enclosing_scroll_frame(RefPtr<ScrollFrame> scroll_frame) { m_enclosing_scroll_frame = scroll_frame; }
void set_enclosing_clip_frame(RefPtr<ClipFrame> clip_frame) { m_enclosing_clip_frame = clip_frame; }
Optional<int> scroll_frame_id() const;
Optional<CSSPixelPoint> enclosing_scroll_frame_offset() const;
Optional<CSSPixelRect> clip_rect() const;
private:
InlinePaintable(Layout::InlineNode const&);
@ -47,9 +50,9 @@ private:
template<typename Callback>
void for_each_fragment(Callback) const;
Optional<int> m_scroll_frame_id;
Optional<CSSPixelPoint> m_enclosing_scroll_frame_offset;
Optional<CSSPixelRect> m_clip_rect;
RefPtr<ScrollFrame const> m_enclosing_scroll_frame;
RefPtr<ClipFrame const> m_enclosing_clip_frame;
Vector<ShadowData> m_box_shadow_data;
Vector<PaintableFragment> m_fragments;

View file

@ -187,6 +187,34 @@ Optional<CSSPixelRect> PaintableBox::get_clip_rect() const
return {};
}
Optional<int> PaintableBox::scroll_frame_id() const
{
if (m_enclosing_scroll_frame)
return m_enclosing_scroll_frame->id;
return {};
}
Optional<CSSPixelPoint> PaintableBox::enclosing_scroll_frame_offset() const
{
if (m_enclosing_scroll_frame)
return m_enclosing_scroll_frame->offset;
return {};
}
Optional<CSSPixelRect> PaintableBox::clip_rect() const
{
if (m_enclosing_clip_frame)
return m_enclosing_clip_frame->rect;
return {};
}
Optional<BorderRadiiData> PaintableBox::corner_clip_radii() const
{
if (m_enclosing_clip_frame)
return m_enclosing_clip_frame->corner_clip_radii;
return {};
}
void PaintableBox::before_paint(PaintContext& context, [[maybe_unused]] PaintPhase phase) const
{
if (!is_visible())
@ -378,15 +406,15 @@ BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders
void PaintableBox::apply_scroll_offset(PaintContext& context, PaintPhase) const
{
if (m_scroll_frame_id.has_value()) {
if (scroll_frame_id().has_value()) {
context.recording_painter().save();
context.recording_painter().set_scroll_frame_id(m_scroll_frame_id.value());
context.recording_painter().set_scroll_frame_id(scroll_frame_id().value());
}
}
void PaintableBox::reset_scroll_offset(PaintContext& context, PaintPhase) const
{
if (m_scroll_frame_id.has_value())
if (scroll_frame_id().has_value())
context.recording_painter().restore();
}
@ -395,8 +423,8 @@ void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase ph
if (!AK::first_is_one_of(phase, PaintPhase::Background, PaintPhase::Border, PaintPhase::Foreground, PaintPhase::Outline))
return;
if (m_clip_rect.has_value()) {
auto overflow_clip_rect = m_clip_rect.value();
if (clip_rect().has_value()) {
auto overflow_clip_rect = clip_rect().value();
for (auto const* ancestor = &this->layout_box(); ancestor; ancestor = ancestor->containing_block()) {
auto affine_transform = Gfx::extract_2d_affine_transform(ancestor->paintable_box()->transform());
if (!affine_transform.is_identity()) {
@ -410,12 +438,12 @@ void PaintableBox::apply_clip_overflow_rect(PaintContext& context, PaintPhase ph
m_clipping_overflow = true;
context.recording_painter().save();
context.recording_painter().add_clip_rect(context.enclosing_device_rect(overflow_clip_rect).to_type<int>());
if (m_corner_clip_radii.has_value()) {
if (corner_clip_radii().has_value()) {
VERIFY(!m_corner_clipper_id.has_value());
m_corner_clipper_id = context.allocate_corner_clipper_id();
auto corner_radii = m_corner_clip_radii->as_corners(context);
auto corner_radii = corner_clip_radii()->as_corners(context);
if (corner_radii.has_any_radius())
context.recording_painter().sample_under_corners(*m_corner_clipper_id, m_corner_clip_radii->as_corners(context), context.rounded_device_rect(overflow_clip_rect).to_type<int>(), CornerClip::Outside);
context.recording_painter().sample_under_corners(*m_corner_clipper_id, corner_clip_radii()->as_corners(context), context.rounded_device_rect(overflow_clip_rect).to_type<int>(), CornerClip::Outside);
}
}
}
@ -427,11 +455,11 @@ void PaintableBox::clear_clip_overflow_rect(PaintContext& context, PaintPhase ph
if (m_clipping_overflow) {
m_clipping_overflow = false;
if (m_corner_clip_radii.has_value()) {
if (corner_clip_radii().has_value()) {
VERIFY(m_corner_clipper_id.has_value());
auto corner_radii = m_corner_clip_radii->as_corners(context);
auto corner_radii = corner_clip_radii()->as_corners(context);
if (corner_radii.has_any_radius())
context.recording_painter().blit_corner_clipping(*m_corner_clipper_id, context.rounded_device_rect(*m_clip_rect).to_type<int>());
context.recording_painter().blit_corner_clipping(*m_corner_clipper_id, context.rounded_device_rect(*clip_rect()).to_type<int>());
m_corner_clipper_id = {};
}
context.recording_painter().restore();
@ -600,6 +628,10 @@ void PaintableWithLines::paint(PaintContext& context, PaintPhase phase) const
Optional<u32> corner_clip_id;
auto clip_box = absolute_padding_box_rect();
if (get_clip_rect().has_value()) {
clip_box.intersect(get_clip_rect().value());
should_clip_overflow = true;
}
if (enclosing_scroll_frame_offset().has_value())
clip_box.translate_by(enclosing_scroll_frame_offset().value());
if (should_clip_overflow) {
@ -687,9 +719,9 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
if (layout_box().is_viewport()) {
auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this));
viewport_paintable.build_stacking_context_tree_if_needed();
HashMap<Painting::PaintableBox const*, Painting::ViewportPaintable::ScrollFrame> scroll_frames;
viewport_paintable.assign_scroll_frame_ids(scroll_frames);
viewport_paintable.assign_clip_rectangles();
viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed();
viewport_paintable.refresh_scroll_state();
viewport_paintable.refresh_clip_state();
return stacking_context()->hit_test(position, type);
}

View file

@ -14,6 +14,16 @@
namespace Web::Painting {
struct ScrollFrame : public RefCounted<ScrollFrame> {
i32 id { -1 };
CSSPixelPoint offset;
};
struct ClipFrame : public RefCounted<ClipFrame> {
CSSPixelRect rect;
Optional<BorderRadiiData> corner_clip_radii;
};
class PaintableBox : public Paintable {
JS_CELL(PaintableBox, Paintable);
@ -193,14 +203,13 @@ public:
Optional<CSSPixelRect> get_clip_rect() const;
void set_clip_rect(Optional<CSSPixelRect> rect) { m_clip_rect = rect; }
void set_scroll_frame_id(int id) { m_scroll_frame_id = id; }
void set_enclosing_scroll_frame_offset(CSSPixelPoint offset) { m_enclosing_scroll_frame_offset = offset; }
void set_corner_clip_radii(BorderRadiiData const& corner_radii) { m_corner_clip_radii = corner_radii; }
void set_enclosing_scroll_frame(RefPtr<ScrollFrame> scroll_frame) { m_enclosing_scroll_frame = scroll_frame; }
void set_enclosing_clip_frame(RefPtr<ClipFrame> clip_frame) { m_enclosing_clip_frame = clip_frame; }
Optional<int> scroll_frame_id() const { return m_scroll_frame_id; }
Optional<CSSPixelPoint> enclosing_scroll_frame_offset() const { return m_enclosing_scroll_frame_offset; }
Optional<CSSPixelRect> clip_rect() const { return m_clip_rect; }
Optional<int> scroll_frame_id() const;
Optional<CSSPixelPoint> enclosing_scroll_frame_offset() const;
Optional<CSSPixelRect> clip_rect() const;
Optional<BorderRadiiData> corner_clip_radii() const;
protected:
explicit PaintableBox(Layout::Box const&);
@ -227,10 +236,8 @@ private:
mutable bool m_clipping_overflow { false };
mutable Optional<u32> m_corner_clipper_id;
Optional<CSSPixelRect> m_clip_rect;
Optional<int> m_scroll_frame_id;
Optional<CSSPixelPoint> m_enclosing_scroll_frame_offset;
Optional<BorderRadiiData> m_corner_clip_radii;
RefPtr<ScrollFrame const> m_enclosing_scroll_frame;
RefPtr<ClipFrame const> m_enclosing_clip_frame;
Optional<BordersDataWithElementKind> m_override_borders_data;
Optional<TableCellCoordinates> m_table_cell_coordinates;

View file

@ -58,37 +58,28 @@ void ViewportPaintable::paint_all_phases(PaintContext& context)
stacking_context()->paint(context);
}
void ViewportPaintable::assign_scroll_frame_ids(HashMap<Painting::PaintableBox const*, ScrollFrame>& scroll_frames) const
void ViewportPaintable::assign_scroll_frames()
{
i32 next_id = 0;
// Collect scroll frames with their offsets (accumulated offset for nested scroll frames).
int next_id = 0;
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
if (paintable_box.has_scrollable_overflow()) {
auto offset = paintable_box.scroll_offset();
auto ancestor = paintable_box.containing_block();
while (ancestor) {
if (ancestor->paintable()->is_paintable_box() && static_cast<PaintableBox const*>(ancestor->paintable())->has_scrollable_overflow())
offset.translate_by(static_cast<PaintableBox const*>(ancestor->paintable())->scroll_offset());
ancestor = ancestor->containing_block();
}
scroll_frames.set(&paintable_box, { .id = next_id++, .offset = -offset });
auto scroll_frame = adopt_ref(*new ScrollFrame());
scroll_frame->id = next_id++;
scroll_state.set(&paintable_box, move(scroll_frame));
}
return TraversalDecision::Continue;
});
// Assign scroll frame id to all paintables contained in a scroll frame.
for_each_in_subtree([&](auto const& paintable) {
for (auto block = paintable.containing_block(); block; block = block->containing_block()) {
auto const& block_paintable_box = *block->paintable_box();
if (auto scroll_frame_id = scroll_frames.get(&block_paintable_box); scroll_frame_id.has_value()) {
if (auto scroll_frame = scroll_state.get(&block_paintable_box); scroll_frame.has_value()) {
if (paintable.is_paintable_box()) {
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
const_cast<PaintableBox&>(paintable_box).set_scroll_frame_id(scroll_frame_id->id);
const_cast<PaintableBox&>(paintable_box).set_enclosing_scroll_frame_offset(scroll_frame_id->offset);
const_cast<PaintableBox&>(paintable_box).set_enclosing_scroll_frame(scroll_frame.value());
} else if (paintable.is_inline_paintable()) {
auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
const_cast<InlinePaintable&>(inline_paintable).set_scroll_frame_id(scroll_frame_id->id);
const_cast<InlinePaintable&>(inline_paintable).set_enclosing_scroll_frame_offset(scroll_frame_id->offset);
const_cast<InlinePaintable&>(inline_paintable).set_enclosing_scroll_frame(scroll_frame.value());
}
break;
}
@ -97,19 +88,64 @@ void ViewportPaintable::assign_scroll_frame_ids(HashMap<Painting::PaintableBox c
});
}
void ViewportPaintable::assign_clip_rectangles()
void ViewportPaintable::assign_clip_frames()
{
HashMap<Paintable const*, CSSPixelRect> clip_rects;
// Calculate clip rects for all boxes that either have hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
auto overflow_x = paintable_box.computed_values().overflow_x();
auto overflow_y = paintable_box.computed_values().overflow_y();
auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible;
if (has_hidden_overflow || paintable_box.get_clip_rect().has_value()) {
auto clip_frame = adopt_ref(*new ClipFrame());
clip_state.set(&paintable_box, move(clip_frame));
}
return TraversalDecision::Continue;
});
for_each_in_subtree([&](auto const& paintable) {
for (auto block = paintable.containing_block(); block; block = block->containing_block()) {
auto const& block_paintable_box = *block->paintable_box();
if (auto clip_frame = clip_state.get(&block_paintable_box); clip_frame.has_value()) {
if (paintable.is_paintable_box()) {
auto const& paintable_box = static_cast<PaintableBox const&>(paintable);
const_cast<PaintableBox&>(paintable_box).set_enclosing_clip_frame(clip_frame.value());
} else if (paintable.is_inline_paintable()) {
auto const& inline_paintable = static_cast<InlinePaintable const&>(paintable);
const_cast<InlinePaintable&>(inline_paintable).set_enclosing_clip_frame(clip_frame.value());
}
break;
}
}
return TraversalDecision::Continue;
});
}
void ViewportPaintable::refresh_scroll_state()
{
for (auto& it : scroll_state) {
auto const& paintable_box = *it.key;
auto& scroll_frame = *it.value;
CSSPixelPoint offset;
for (auto const* block = &paintable_box.layout_box(); block; block = block->containing_block()) {
auto const& block_paintable_box = *block->paintable_box();
offset.translate_by(block_paintable_box.scroll_offset());
}
scroll_frame.offset = -offset;
}
}
void ViewportPaintable::refresh_clip_state()
{
for (auto& it : clip_state) {
auto const& paintable_box = *it.key;
auto& clip_frame = *it.value;
auto overflow_x = paintable_box.computed_values().overflow_x();
auto overflow_y = paintable_box.computed_values().overflow_y();
// Start from CSS clip property if it exists.
Optional<CSSPixelRect> clip_rect = paintable_box.get_clip_rect();
// FIXME: Support overflow clip in one direction only.
if (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) {
auto overflow_clip_rect = paintable_box.compute_absolute_padding_rect_with_css_transform_applied();
for (auto block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
for (auto const* block = &paintable_box.layout_box(); !block->is_viewport(); block = block->containing_block()) {
auto const& block_paintable_box = *block->paintable_box();
auto block_overflow_x = block_paintable_box.computed_values().overflow_x();
auto block_overflow_y = block_paintable_box.computed_values().overflow_y();
@ -120,39 +156,15 @@ void ViewportPaintable::assign_clip_rectangles()
}
clip_rect = overflow_clip_rect;
}
if (clip_rect.has_value())
clip_rects.set(&paintable_box, *clip_rect);
return TraversalDecision::Continue;
});
// Assign clip rects to all paintable boxes contained by a box with a hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
Optional<CSSPixelRect> clip_rect = paintable_box.get_clip_rect();
for (auto block = paintable_box.containing_block(); block; block = block->containing_block()) {
if (auto containing_block_clip_rect = clip_rects.get(block->paintable()); containing_block_clip_rect.has_value()) {
auto border_radii_data = block->paintable_box()->normalized_border_radii_data(ShrinkRadiiForBorders::Yes);
if (border_radii_data.has_any_radius()) {
// FIXME: Border radii of all boxes in containing block chain should be taken into account instead of just the closest one.
const_cast<PaintableBox&>(paintable_box).set_corner_clip_radii(border_radii_data);
}
clip_rect = *containing_block_clip_rect;
break;
}
auto border_radii_data = paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes);
if (border_radii_data.has_any_radius()) {
// FIXME: Border radii of all boxes in containing block chain should be taken into account.
clip_frame.corner_clip_radii = border_radii_data;
}
const_cast<PaintableBox&>(paintable_box).set_clip_rect(clip_rect);
return TraversalDecision::Continue;
});
// Assign clip rects to all inline paintables contained by a box with hidden overflow or a CSS clip property.
for_each_in_subtree_of_type<InlinePaintable>([&](auto const& paintable_box) {
for (auto block = paintable_box.containing_block(); block; block = block->containing_block()) {
if (auto clip_rect = clip_rects.get(block->paintable()); clip_rect.has_value()) {
const_cast<InlinePaintable&>(paintable_box).set_clip_rect(clip_rect);
break;
}
}
return TraversalDecision::Continue;
});
clip_frame.rect = *clip_rect;
}
}
static Painting::BorderRadiiData normalize_border_radii_data(Layout::Node const& node, CSSPixelRect const& rect, CSS::BorderRadiusData top_left_radius, CSS::BorderRadiusData top_right_radius, CSS::BorderRadiusData bottom_right_radius, CSS::BorderRadiusData bottom_left_radius)

View file

@ -20,12 +20,14 @@ public:
void paint_all_phases(PaintContext&);
void build_stacking_context_tree_if_needed();
struct ScrollFrame {
i32 id { -1 };
CSSPixelPoint offset;
};
void assign_scroll_frame_ids(HashMap<Painting::PaintableBox const*, ScrollFrame>&) const;
void assign_clip_rectangles();
HashMap<PaintableBox const*, RefPtr<ScrollFrame>> scroll_state;
void assign_scroll_frames();
void refresh_scroll_state();
HashMap<PaintableBox const*, RefPtr<ClipFrame>> clip_state;
void assign_clip_frames();
void refresh_clip_state();
void resolve_paint_only_properties();
private: