Files
ladybird/Libraries/LibWeb/Painting/DisplayList.cpp
Jelle Raaijmakers f5bb5d0797 LibWeb: Don't tear down an active effect saveLayer during culling check
Before applying an effect context (filter, opacity, blend-mode), the
display list player runs a culling check by tentatively switching to the
effect's parent first. When the effect was already on the stack (i.e.
the current context was a descendant of the target) that walk restored
the saveLayer prematurely. The filter was then applied to a partial
batch of commands, and subsequent commands opened a fresh saveLayer for
a second, independent application, producing visibly wrong compositing.

Fold the culling check into switch_to_context: while walking down to
apply contexts, check the bounding rect right before each EffectsData
node and abort if the command is fully clipped. Walks that only go up
never traverse an EffectsData node, so an already-applied effect is
never torn down.

This regressed in cd0705334b.
2026-04-28 16:14:44 +02:00

229 lines
9.6 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) -> IterationDecision {
if (!target_index.value() || target_index == common_ancestor_index)
return IterationDecision::Continue;
if (self(common_ancestor_index, visual_context_tree.node_at(target_index).parent_index, callback) == IterationDecision::Break)
return IterationDecision::Break;
return 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;
// OPTIMIZATION: When walking down to apply effects (opacity, filters, blend modes), check culling before applying
// each effect. Effects don't affect clip state, so the culling check is valid before applying them.
// This avoids expensive saveLayer/restore cycles for off-screen elements with effects like blur.
enum class SwitchResult : u8 {
Switched,
CulledByEffect,
};
auto switch_to_context = [&](VisualContextIndex target_index, Optional<Gfx::IntRect> bounding_rect = {}) -> SwitchResult {
if (applied_context_index == target_index)
return SwitchResult::Switched;
auto common_ancestor_index = visual_context_tree.find_common_ancestor(applied_context_index, target_index);
size_t const 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--;
}
auto result = SwitchResult::Switched;
for_each_node_from_common_ancestor_to_target(common_ancestor_index, target_index, [&](AccumulatedVisualContextNode const& node) {
if (bounding_rect.has_value() && node.data.has<EffectsData>()) {
if (bounding_rect->is_empty() || would_be_fully_clipped_by_painter(*bounding_rect)) {
result = SwitchResult::CulledByEffect;
return IterationDecision::Break;
}
}
apply_accumulated_visual_context(node);
applied_depth++;
return IterationDecision::Continue;
});
if (result == SwitchResult::Switched)
applied_context_index = target_index;
return result;
};
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);
if (switch_to_context(context_index, bounding_rect) == SwitchResult::CulledByEffect)
continue;
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--;
}
}
}