Skip to content

Commit ba2926f

Browse files
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.
1 parent 4acde45 commit ba2926f

File tree

6 files changed

+84
-14
lines changed

6 files changed

+84
-14
lines changed

Libraries/LibWeb/Painting/DisplayList.cpp

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
125125

126126
VERIFY(!m_surfaces.is_empty());
127127

128+
auto translate_command_by_scroll = [&](auto& command, int scroll_frame_id) {
129+
auto cumulative_offset = scroll_state.cumulative_offset_for_frame_with_id(scroll_frame_id);
130+
auto scroll_offset = cumulative_offset.to_type<double>().scaled(device_pixels_per_css_pixel).to_type<int>();
131+
command.visit(
132+
[scroll_offset](auto& command) {
133+
if constexpr (requires { command.translate_by(scroll_offset); }) {
134+
command.translate_by(scroll_offset);
135+
}
136+
});
137+
};
138+
139+
auto compute_stacking_context_bounds = [&](PushStackingContext const& push_stacking_context, size_t push_stacking_context_index) {
140+
Gfx::IntRect bounding_rect;
141+
display_list.for_each_command_in_range(push_stacking_context_index + 1, push_stacking_context.matching_pop_index, [&](auto command, auto scroll_frame_id) {
142+
if (scroll_frame_id.has_value())
143+
translate_command_by_scroll(command, scroll_frame_id.value());
144+
bounding_rect.unite(*command_bounding_rectangle(command));
145+
return IterationDecision::Continue;
146+
});
147+
return bounding_rect;
148+
};
149+
128150
Vector<RefPtr<ClipFrame const>> clip_frames_stack;
129151
clip_frames_stack.append({});
130152
for (size_t command_index = 0; command_index < commands.size(); command_index++) {
@@ -164,18 +186,19 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
164186
}
165187
}
166188

167-
if (scroll_frame_id.has_value()) {
168-
auto cumulative_offset = scroll_state.cumulative_offset_for_frame_with_id(scroll_frame_id.value());
169-
auto scroll_offset = cumulative_offset.to_type<double>().scaled(device_pixels_per_css_pixel).to_type<int>();
170-
command.visit(
171-
[&](auto& command) {
172-
if constexpr (requires { command.translate_by(scroll_offset); }) {
173-
command.translate_by(scroll_offset);
174-
}
175-
});
176-
}
189+
if (scroll_frame_id.has_value())
190+
translate_command_by_scroll(command, scroll_frame_id.value());
177191

178192
auto bounding_rect = command_bounding_rectangle(command);
193+
194+
if (command.has<PushStackingContext>()) {
195+
auto& push_stacking_context = command.get<PushStackingContext>();
196+
if (push_stacking_context.can_aggregate_children_bounds) {
197+
bounding_rect = compute_stacking_context_bounds(push_stacking_context, command_index);
198+
push_stacking_context.bounding_rect = bounding_rect;
199+
}
200+
}
201+
179202
if (bounding_rect.has_value() && (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))) {
180203
// Any clip or mask that's located outside of the visible region is equivalent to a simple clip-rect,
181204
// so replace it with one to avoid doing unnecessary work.
@@ -186,6 +209,11 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
186209
add_clip_rect({ bounding_rect.release_value() });
187210
}
188211
}
212+
if (command.has<PushStackingContext>()) {
213+
auto pop_stacking_context = command.get<PushStackingContext>().matching_pop_index;
214+
command_index = pop_stacking_context;
215+
(void)clip_frames_stack.take_last();
216+
}
189217
continue;
190218
}
191219

Libraries/LibWeb/Painting/DisplayList.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,21 @@ class DisplayList : public AtomicRefCounted<DisplayList> {
9494
DisplayListCommand command;
9595
};
9696

97-
AK::SegmentedVector<DisplayListCommandWithScrollAndClip, 512> const& commands() const { return m_commands; }
97+
auto& commands(Badge<DisplayListRecorder>) { return m_commands; }
98+
auto const& commands() const { return m_commands; }
9899
double device_pixels_per_css_pixel() const { return m_device_pixels_per_css_pixel; }
99100

100101
String dump() const;
101102

103+
template<typename Callback>
104+
void for_each_command_in_range(size_t start, size_t end, Callback callback)
105+
{
106+
for (auto index = start; index < end; ++index) {
107+
if (callback(m_commands[index].command, m_commands[index].scroll_frame_id) == IterationDecision::Break)
108+
break;
109+
}
110+
}
111+
102112
private:
103113
DisplayList(double device_pixels_per_css_pixel)
104114
: m_device_pixels_per_css_pixel(device_pixels_per_css_pixel)

Libraries/LibWeb/Painting/DisplayListCommand.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ struct PushStackingContext {
145145
StackingContextTransform transform;
146146
Optional<Gfx::Path> clip_path = {};
147147

148+
size_t matching_pop_index { 0 };
149+
bool can_aggregate_children_bounds { false };
150+
Optional<Gfx::IntRect> bounding_rect {};
151+
148152
void translate_by(Gfx::IntPoint const& offset)
149153
{
150154
transform.origin.translate_by(offset.to_type<float>());

Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,12 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com
216216
paint.setAlphaf(command.opacity);
217217
paint.setBlender(Gfx::to_skia_blender(command.compositing_and_blending_operator));
218218

219-
// FIXME: If we knew the bounds of the stacking context including any transformed descendants etc,
220-
// we could use saveLayer with a bounds rect. For now, we pass nullptr and let Skia figure it out.
221-
canvas.saveLayer(nullptr, &paint);
219+
if (command.bounding_rect.has_value()) {
220+
auto bounds = to_skia_rect(command.bounding_rect.value());
221+
canvas.saveLayer(bounds, &paint);
222+
} else {
223+
canvas.saveLayer(nullptr, &paint);
224+
}
222225
} else {
223226
canvas.save();
224227
}

Libraries/LibWeb/Painting/DisplayListRecorder.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,36 @@ void DisplayListRecorder::push_stacking_context(PushStackingContextParams params
313313
.transform = params.transform,
314314
.clip_path = params.clip_path });
315315
m_clip_frame_stack.append({});
316+
m_push_sc_index_stack.append(m_display_list.commands().size() - 1);
317+
}
318+
319+
static bool command_has_bounding_rectangle(DisplayListCommand const& command)
320+
{
321+
return command.visit(
322+
[&](auto const& command) {
323+
if constexpr (requires { command.bounding_rect(); })
324+
return true;
325+
return false;
326+
});
316327
}
317328

318329
void DisplayListRecorder::pop_stacking_context()
319330
{
320331
APPEND(PopStackingContext {});
321332
(void)m_clip_frame_stack.take_last();
333+
auto pop_index = m_display_list.commands().size() - 1;
334+
auto push_index = m_push_sc_index_stack.take_last();
335+
auto& push_stacking_context = m_display_list.commands({})[push_index].command.get<PushStackingContext>();
336+
push_stacking_context.matching_pop_index = m_display_list.commands().size() - 1;
337+
338+
push_stacking_context.can_aggregate_children_bounds = true;
339+
m_display_list.for_each_command_in_range(push_index + 1, pop_index, [&](auto const& command, auto) {
340+
if (!command_has_bounding_rectangle(command)) {
341+
push_stacking_context.can_aggregate_children_bounds = false;
342+
return IterationDecision::Break;
343+
}
344+
return IterationDecision::Continue;
345+
});
322346
}
323347

324348
void DisplayListRecorder::apply_backdrop_filter(Gfx::IntRect const& backdrop_region, BorderRadiiData const& border_radii_data, Gfx::Filter const& backdrop_filter)

Libraries/LibWeb/Painting/DisplayListRecorder.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class WEB_API DisplayListRecorder {
148148
private:
149149
Vector<Optional<i32>> m_scroll_frame_id_stack;
150150
Vector<RefPtr<ClipFrame const>> m_clip_frame_stack;
151+
Vector<size_t> m_push_sc_index_stack;
151152
DisplayList& m_display_list;
152153
};
153154

0 commit comments

Comments
 (0)