LibWeb: Calculate and use bounds for "simple" stacking contexts

Teach the display list executor to derive a bounding rectangle for
stacking contexts whose inner commands can all report bounds, that is,
most contexts without nested stacking contexts.

This yields a large performance improvement on https://tc39.es/ecma262/
where the display list contains thousands of groups like:
```
PushStackingContext blending=Multiply
    DrawGlyphRun
PopStackingContext
```
Previously, `PushStackingContext` triggered an unbounded `saveLayer()`
even when the glyph run lies wholly outside the viewport. With this
change, we (1) cull stacking contexts that fall outside the viewport and
(2) provide bounds to `saveLayer()` when they are visible.

With this change rendering thread goes from 70% to 1% in profiles of
https://tc39.es/ecma262/. Also makes a huge performance difference on
Discord.
This commit is contained in:
Aliaksandr Kalenik 2025-09-24 19:33:54 +02:00 committed by Alexander Kalenik
parent 4acde45d5e
commit ba2926f8b3
6 changed files with 84 additions and 14 deletions

View File

@ -125,6 +125,28 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
VERIFY(!m_surfaces.is_empty()); VERIFY(!m_surfaces.is_empty());
auto translate_command_by_scroll = [&](auto& command, int scroll_frame_id) {
auto cumulative_offset = scroll_state.cumulative_offset_for_frame_with_id(scroll_frame_id);
auto scroll_offset = cumulative_offset.to_type<double>().scaled(device_pixels_per_css_pixel).to_type<int>();
command.visit(
[scroll_offset](auto& command) {
if constexpr (requires { command.translate_by(scroll_offset); }) {
command.translate_by(scroll_offset);
}
});
};
auto compute_stacking_context_bounds = [&](PushStackingContext const& push_stacking_context, size_t push_stacking_context_index) {
Gfx::IntRect bounding_rect;
display_list.for_each_command_in_range(push_stacking_context_index + 1, push_stacking_context.matching_pop_index, [&](auto command, auto scroll_frame_id) {
if (scroll_frame_id.has_value())
translate_command_by_scroll(command, scroll_frame_id.value());
bounding_rect.unite(*command_bounding_rectangle(command));
return IterationDecision::Continue;
});
return bounding_rect;
};
Vector<RefPtr<ClipFrame const>> clip_frames_stack; Vector<RefPtr<ClipFrame const>> clip_frames_stack;
clip_frames_stack.append({}); clip_frames_stack.append({});
for (size_t command_index = 0; command_index < commands.size(); command_index++) { for (size_t command_index = 0; command_index < commands.size(); command_index++) {
@ -164,18 +186,19 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
} }
} }
if (scroll_frame_id.has_value()) { if (scroll_frame_id.has_value())
auto cumulative_offset = scroll_state.cumulative_offset_for_frame_with_id(scroll_frame_id.value()); translate_command_by_scroll(command, scroll_frame_id.value());
auto scroll_offset = cumulative_offset.to_type<double>().scaled(device_pixels_per_css_pixel).to_type<int>();
command.visit(
[&](auto& command) {
if constexpr (requires { command.translate_by(scroll_offset); }) {
command.translate_by(scroll_offset);
}
});
}
auto bounding_rect = command_bounding_rectangle(command); auto bounding_rect = command_bounding_rectangle(command);
if (command.has<PushStackingContext>()) {
auto& push_stacking_context = command.get<PushStackingContext>();
if (push_stacking_context.can_aggregate_children_bounds) {
bounding_rect = compute_stacking_context_bounds(push_stacking_context, command_index);
push_stacking_context.bounding_rect = bounding_rect;
}
}
if (bounding_rect.has_value() && (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))) { if (bounding_rect.has_value() && (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))) {
// Any clip or mask that's located outside of the visible region is equivalent to a simple clip-rect, // Any clip or mask that's located outside of the visible region is equivalent to a simple clip-rect,
// so replace it with one to avoid doing unnecessary work. // so replace it with one to avoid doing unnecessary work.
@ -186,6 +209,11 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
add_clip_rect({ bounding_rect.release_value() }); add_clip_rect({ bounding_rect.release_value() });
} }
} }
if (command.has<PushStackingContext>()) {
auto pop_stacking_context = command.get<PushStackingContext>().matching_pop_index;
command_index = pop_stacking_context;
(void)clip_frames_stack.take_last();
}
continue; continue;
} }

View File

@ -94,11 +94,21 @@ public:
DisplayListCommand command; DisplayListCommand command;
}; };
AK::SegmentedVector<DisplayListCommandWithScrollAndClip, 512> const& commands() const { return m_commands; } auto& commands(Badge<DisplayListRecorder>) { return m_commands; }
auto const& commands() const { return m_commands; }
double device_pixels_per_css_pixel() const { return m_device_pixels_per_css_pixel; } double device_pixels_per_css_pixel() const { return m_device_pixels_per_css_pixel; }
String dump() const; String dump() const;
template<typename Callback>
void for_each_command_in_range(size_t start, size_t end, Callback callback)
{
for (auto index = start; index < end; ++index) {
if (callback(m_commands[index].command, m_commands[index].scroll_frame_id) == IterationDecision::Break)
break;
}
}
private: private:
DisplayList(double device_pixels_per_css_pixel) DisplayList(double device_pixels_per_css_pixel)
: m_device_pixels_per_css_pixel(device_pixels_per_css_pixel) : m_device_pixels_per_css_pixel(device_pixels_per_css_pixel)

View File

@ -145,6 +145,10 @@ struct PushStackingContext {
StackingContextTransform transform; StackingContextTransform transform;
Optional<Gfx::Path> clip_path = {}; Optional<Gfx::Path> clip_path = {};
size_t matching_pop_index { 0 };
bool can_aggregate_children_bounds { false };
Optional<Gfx::IntRect> bounding_rect {};
void translate_by(Gfx::IntPoint const& offset) void translate_by(Gfx::IntPoint const& offset)
{ {
transform.origin.translate_by(offset.to_type<float>()); transform.origin.translate_by(offset.to_type<float>());

View File

@ -216,9 +216,12 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com
paint.setAlphaf(command.opacity); paint.setAlphaf(command.opacity);
paint.setBlender(Gfx::to_skia_blender(command.compositing_and_blending_operator)); paint.setBlender(Gfx::to_skia_blender(command.compositing_and_blending_operator));
// FIXME: If we knew the bounds of the stacking context including any transformed descendants etc, if (command.bounding_rect.has_value()) {
// we could use saveLayer with a bounds rect. For now, we pass nullptr and let Skia figure it out. auto bounds = to_skia_rect(command.bounding_rect.value());
canvas.saveLayer(bounds, &paint);
} else {
canvas.saveLayer(nullptr, &paint); canvas.saveLayer(nullptr, &paint);
}
} else { } else {
canvas.save(); canvas.save();
} }

View File

@ -313,12 +313,36 @@ void DisplayListRecorder::push_stacking_context(PushStackingContextParams params
.transform = params.transform, .transform = params.transform,
.clip_path = params.clip_path }); .clip_path = params.clip_path });
m_clip_frame_stack.append({}); m_clip_frame_stack.append({});
m_push_sc_index_stack.append(m_display_list.commands().size() - 1);
}
static bool command_has_bounding_rectangle(DisplayListCommand const& command)
{
return command.visit(
[&](auto const& command) {
if constexpr (requires { command.bounding_rect(); })
return true;
return false;
});
} }
void DisplayListRecorder::pop_stacking_context() void DisplayListRecorder::pop_stacking_context()
{ {
APPEND(PopStackingContext {}); APPEND(PopStackingContext {});
(void)m_clip_frame_stack.take_last(); (void)m_clip_frame_stack.take_last();
auto pop_index = m_display_list.commands().size() - 1;
auto push_index = m_push_sc_index_stack.take_last();
auto& push_stacking_context = m_display_list.commands({})[push_index].command.get<PushStackingContext>();
push_stacking_context.matching_pop_index = m_display_list.commands().size() - 1;
push_stacking_context.can_aggregate_children_bounds = true;
m_display_list.for_each_command_in_range(push_index + 1, pop_index, [&](auto const& command, auto) {
if (!command_has_bounding_rectangle(command)) {
push_stacking_context.can_aggregate_children_bounds = false;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
} }
void DisplayListRecorder::apply_backdrop_filter(Gfx::IntRect const& backdrop_region, BorderRadiiData const& border_radii_data, Gfx::Filter const& backdrop_filter) void DisplayListRecorder::apply_backdrop_filter(Gfx::IntRect const& backdrop_region, BorderRadiiData const& border_radii_data, Gfx::Filter const& backdrop_filter)

View File

@ -148,6 +148,7 @@ public:
private: private:
Vector<Optional<i32>> m_scroll_frame_id_stack; Vector<Optional<i32>> m_scroll_frame_id_stack;
Vector<RefPtr<ClipFrame const>> m_clip_frame_stack; Vector<RefPtr<ClipFrame const>> m_clip_frame_stack;
Vector<size_t> m_push_sc_index_stack;
DisplayList& m_display_list; DisplayList& m_display_list;
}; };