Files
ladybird/Libraries/LibWeb/Painting/DisplayList.cpp
Aliaksandr Kalenik d9eec0d204 LibWeb: Replace ScrollStateSnapshotByDisplayList with a plain snapshot
Cleanup following the per-Navigable rasterization split: since each
Navigable now rasterizes its own display list independently, the HashMap
keyed on display list was always populated with exactly one entry. Pass
the ScrollStateSnapshot directly through the display list player and
rendering thread instead.
2026-04-07 15:09:43 +02:00

223 lines
9.5 KiB
C++

/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TemporaryChange.h>
#include <LibGfx/PaintingSurface.h>
#include <LibWeb/Painting/DisplayList.h>
namespace Web::Painting {
bool DisplayList::append(DisplayListCommand&& command, VisualContextIndex context_index)
{
if (context_index.value() && m_visual_context_tree->has_empty_effective_clip(context_index))
return false;
m_commands.append({ context_index, move(command) });
return true;
}
static Optional<Gfx::IntRect> command_bounding_rectangle(DisplayListCommand const& command)
{
return command.visit(
[&](auto const& command) -> Optional<Gfx::IntRect> {
if constexpr (requires { command.bounding_rect(); })
return command.bounding_rect();
else
return {};
});
}
static bool command_is_clip(DisplayListCommand const& command)
{
return command.visit(
[&](auto const& command) -> bool {
if constexpr (requires { command.is_clip(); })
return command.is_clip();
else
return false;
});
}
void DisplayListPlayer::execute(DisplayList& display_list, ScrollStateSnapshot const& scroll_state_snapshot, RefPtr<Gfx::PaintingSurface> surface)
{
if (surface) {
surface->lock_context();
}
m_surface = surface;
execute_impl(display_list, scroll_state_snapshot);
if (surface)
flush();
m_surface = nullptr;
if (surface) {
surface->unlock_context();
}
}
void DisplayListPlayer::execute_display_list_into_surface(DisplayList& display_list, Gfx::PaintingSurface& target_surface)
{
TemporaryChange surface_change { m_surface, RefPtr<Gfx::PaintingSurface> { target_surface } };
ScrollStateSnapshot scroll_state_snapshot;
execute_impl(display_list, scroll_state_snapshot);
}
void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnapshot const& scroll_state)
{
auto const& commands = display_list.commands();
auto const& visual_context_tree = display_list.visual_context_tree();
VERIFY(m_surface);
auto for_each_node_from_common_ancestor_to_target = [&](this auto const& self, VisualContextIndex common_ancestor_index, VisualContextIndex target_index, auto&& callback) -> void {
if (!target_index.value() || target_index == common_ancestor_index)
return;
self(common_ancestor_index, visual_context_tree.node_at(target_index).parent_index, callback);
callback(visual_context_tree.node_at(target_index));
};
auto apply_accumulated_visual_context = [&](AccumulatedVisualContextNode const& node) {
node.data.visit(
[&](EffectsData const& effects) {
apply_effects({ .opacity = effects.opacity, .compositing_and_blending_operator = effects.blend_mode, .filter = effects.gfx_filter });
},
[&](PerspectiveData const& perspective) {
save({});
apply_transform({ 0, 0 }, perspective.matrix);
},
[&](ScrollData const& scroll) {
save({});
auto offset = scroll_state.device_offset_for_index(scroll.scroll_frame_index);
if (!offset.is_zero())
translate({ .delta = offset.to_type<int>() });
},
[&](TransformData const& transform) {
save({});
apply_transform(transform.origin, transform.matrix);
},
[&](ClipData const& clip) {
save({});
if (clip.corner_radii.has_any_radius())
add_rounded_rect_clip({ .corner_radii = clip.corner_radii, .border_rect = clip.rect.to_type<int>(), .corner_clip = CornerClip::Outside });
else
add_clip_rect({ .rect = clip.rect.to_type<int>() });
},
[&](ClipPathData const& clip_path) {
save({});
add_clip_path(clip_path.path);
});
};
VisualContextIndex applied_context_index;
size_t applied_depth = 0;
auto switch_to_context = [&](VisualContextIndex target_index) {
if (applied_context_index == target_index)
return;
auto common_ancestor_index = visual_context_tree.find_common_ancestor(applied_context_index, target_index);
size_t common_ancestor_depth = common_ancestor_index.value() ? visual_context_tree.node_at(common_ancestor_index).depth : 0;
while (applied_depth > common_ancestor_depth) {
restore({});
applied_depth--;
}
for_each_node_from_common_ancestor_to_target(common_ancestor_index, target_index, [&](AccumulatedVisualContextNode const& node) {
apply_accumulated_visual_context(node);
applied_depth++;
});
applied_context_index = target_index;
};
for (size_t command_index = 0; command_index < commands.size(); command_index++) {
auto const& [context_index, command] = commands[command_index];
auto bounding_rect = command_bounding_rectangle(command);
// OPTIMIZATION: If the leaf context is an effect and we're switching to a new context,
// check culling before applying it. Effects (opacity, filters, blend modes) don't affect
// clip state, so would_be_fully_clipped_by_painter() returns the same result before and after
// applying effects.
// This avoids expensive saveLayer/restore cycles for off-screen elements with effects like blur.
// NOTE: We must not do this for consecutive commands with the same context, as that would incorrectly restore
// and re-apply the effect layer, breaking blend mode compositing.
if (context_index.value() && applied_context_index != context_index && visual_context_tree.is_effect(context_index) && bounding_rect.has_value()) {
switch_to_context(visual_context_tree.node_at(context_index).parent_index);
if (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))
continue;
}
switch_to_context(context_index);
if (command.has<PaintScrollBar>()) {
auto translated_command = command;
auto& paint_scroll_bar = translated_command.get<PaintScrollBar>();
auto device_offset = scroll_state.device_offset_for_index(paint_scroll_bar.scroll_frame_index);
if (paint_scroll_bar.vertical)
paint_scroll_bar.thumb_rect.translate_by(0, static_cast<int>(-device_offset.y() * paint_scroll_bar.scroll_size));
else
paint_scroll_bar.thumb_rect.translate_by(static_cast<int>(-device_offset.x() * paint_scroll_bar.scroll_size), 0);
paint_scrollbar(paint_scroll_bar);
continue;
}
if (bounding_rect.has_value() && (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect))) {
// Any clip 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.
if (command_is_clip(command)) {
if (command.has<AddClipRect>()) {
add_clip_rect(command.get<AddClipRect>());
} else {
add_clip_rect({ bounding_rect.release_value() });
}
}
continue;
}
#define HANDLE_COMMAND(command_type, executor_method) \
if (command.has<command_type>()) { \
executor_method(command.get<command_type>()); \
}
// clang-format off
HANDLE_COMMAND(DrawGlyphRun, draw_glyph_run)
else HANDLE_COMMAND(FillRect, fill_rect)
else HANDLE_COMMAND(DrawScaledImmutableBitmap, draw_scaled_immutable_bitmap)
else HANDLE_COMMAND(DrawRepeatedImmutableBitmap, draw_repeated_immutable_bitmap)
else HANDLE_COMMAND(DrawExternalContent, draw_external_content)
else HANDLE_COMMAND(AddClipRect, add_clip_rect)
else HANDLE_COMMAND(Save, save)
else HANDLE_COMMAND(SaveLayer, save_layer)
else HANDLE_COMMAND(Restore, restore)
else HANDLE_COMMAND(Translate, translate)
else HANDLE_COMMAND(PaintLinearGradient, paint_linear_gradient)
else HANDLE_COMMAND(PaintRadialGradient, paint_radial_gradient)
else HANDLE_COMMAND(PaintConicGradient, paint_conic_gradient)
else HANDLE_COMMAND(PaintOuterBoxShadow, paint_outer_box_shadow)
else HANDLE_COMMAND(PaintInnerBoxShadow, paint_inner_box_shadow)
else HANDLE_COMMAND(PaintTextShadow, paint_text_shadow)
else HANDLE_COMMAND(FillRectWithRoundedCorners, fill_rect_with_rounded_corners)
else HANDLE_COMMAND(FillPath, fill_path)
else HANDLE_COMMAND(StrokePath, stroke_path)
else HANDLE_COMMAND(DrawEllipse, draw_ellipse)
else HANDLE_COMMAND(FillEllipse, fill_ellipse)
else HANDLE_COMMAND(DrawLine, draw_line)
else HANDLE_COMMAND(ApplyBackdropFilter, apply_backdrop_filter)
else HANDLE_COMMAND(DrawRect, draw_rect)
else HANDLE_COMMAND(AddRoundedRectClip, add_rounded_rect_clip)
else HANDLE_COMMAND(PaintNestedDisplayList, paint_nested_display_list)
else HANDLE_COMMAND(ApplyEffects, apply_effects)
else VERIFY_NOT_REACHED();
// clang-format on
}
while (applied_depth > 0) {
restore({});
applied_depth--;
}
}
}