LibWeb: Store visual context nodes in arena-based tree

Replace per-node heap-allocated AtomicRefCounted
AccumulatedVisualContext objects with a single contiguous Vector inside
AccumulatedVisualContextTree. All nodes for a frame are now stored in
one allocation, using type-safe VisualContextIndex instead of RefPtr
pointers.

This reduces allocation churn, improves cache locality, and opens the
door for future snapshotting of visual context state — similar to how
scroll offsets are snapshotted today.
This commit is contained in:
Aliaksandr Kalenik 2026-03-11 00:21:46 +01:00 committed by Alexander Kalenik
parent 834675aea3
commit 9d2ebe90ed
20 changed files with 258 additions and 207 deletions

View file

@ -101,7 +101,7 @@ Optional<Gfx::ImageCursor> CursorStyleValue::make_image_cursor(Layout::NodeWithS
painter->clear_rect(bitmap.rect().to_type<float>(), Color::Transparent);
// Paint the cursor into a bitmap.
auto display_list = Painting::DisplayList::create();
auto display_list = Painting::DisplayList::create(Painting::AccumulatedVisualContextTree::create());
Painting::DisplayListRecorder display_list_recorder(display_list);
DisplayListRecordingContext paint_context { display_list_recorder, document.page().palette(), document.page().client().device_pixels_per_css_pixel(), document.page().chrome_metrics() };

View file

@ -7344,7 +7344,10 @@ RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig co
if (m_cached_display_list && m_cached_display_list_paint_config == config)
return m_cached_display_list;
auto display_list = Painting::DisplayList::create();
update_paint_and_hit_testing_properties_if_needed();
VERIFY(paintable());
auto display_list = Painting::DisplayList::create(*paintable()->visual_context_tree());
Painting::DisplayListRecorder display_list_recorder(display_list);
// https://drafts.csswg.org/css-color-adjust-1/#color-scheme-effect
@ -7380,17 +7383,12 @@ RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig co
display_list_recorder.fill_rect(bitmap_rect, CSS::SystemColor::canvas(color_scheme));
display_list_recorder.fill_rect(bitmap_rect, background_color());
if (!paintable()) {
VERIFY_NOT_REACHED();
}
Web::DisplayListRecordingContext context(display_list_recorder, page().palette(), page().client().device_pixels_per_css_pixel(), page().chrome_metrics());
context.set_device_viewport_rect(viewport_rect);
context.set_should_show_line_box_borders(config.should_show_line_box_borders);
context.set_should_paint_overlay(config.paint_overlay);
update_paint_and_hit_testing_properties_if_needed();
auto& viewport_paintable = *paintable();
viewport_paintable.paint_all_phases(context);
@ -7642,41 +7640,45 @@ String Document::dump_display_list()
HashMap<size_t, Painting::PaintableBox const*> context_id_to_paintable;
viewport_paintable->for_each_in_inclusive_subtree_of_type<Painting::PaintableBox>([&](auto const& paintable_box) {
if (auto context = paintable_box.accumulated_visual_context())
(void)context_id_to_paintable.try_set(context->id(), &paintable_box);
auto visual_context_index = paintable_box.accumulated_visual_context_index();
if (visual_context_index.value())
(void)context_id_to_paintable.try_set(visual_context_index.value(), &paintable_box);
return TraversalDecision::Continue;
});
HashTable<Painting::AccumulatedVisualContext const*> visited;
HashMap<Painting::AccumulatedVisualContext const*, Vector<Painting::AccumulatedVisualContext const*>> children;
Vector<Painting::AccumulatedVisualContext const*> root_contexts;
StringBuilder builder;
builder.append("AccumulatedVisualContext Tree:\n"sv);
auto const& visual_context_tree = display_list->visual_context_tree();
HashTable<size_t> visited;
HashMap<size_t, Vector<size_t>> children;
Vector<size_t> root_contexts;
for (auto const& item : display_list->commands()) {
if (!item.context)
if (!item.context_index.value())
continue;
for (auto const* node = item.context.ptr(); node && !visited.contains(node); node = node->parent().ptr()) {
visited.set(node);
if (auto const* parent = node->parent().ptr())
children.ensure(parent).append(node);
else if (!root_contexts.contains_slow(node))
root_contexts.append(node);
for (size_t node_index = item.context_index.value(); node_index && !visited.contains(node_index); node_index = visual_context_tree.node_at(Painting::VisualContextIndex(node_index)).parent_index.value()) {
visited.set(node_index);
auto parent = visual_context_tree.node_at(Painting::VisualContextIndex(node_index)).parent_index.value();
if (parent)
children.ensure(parent).append(node_index);
else if (!root_contexts.contains_slow(node_index))
root_contexts.append(node_index);
}
}
StringBuilder builder;
builder.append("AccumulatedVisualContext Tree:\n"sv);
Function<void(Painting::AccumulatedVisualContext const*, size_t)> dump_context = [&](auto const* node, size_t indent) {
Function<void(size_t, size_t)> dump_context = [&](size_t node_index, size_t indent) {
builder.append_repeated(' ', indent * 2);
builder.appendff("[{}] ", node->id());
node->dump(builder);
if (auto it = context_id_to_paintable.find(node->id()); it != context_id_to_paintable.end())
builder.appendff("[{}] ", node_index);
visual_context_tree.dump(Painting::VisualContextIndex(node_index), builder);
if (auto it = context_id_to_paintable.find(node_index); it != context_id_to_paintable.end())
builder.appendff(" ({})", it->value->debug_description());
builder.append('\n');
for (auto const* child : children.get(node).value_or({}))
dump_context(child, indent + 1);
for (auto child_node_index : children.get(node_index).value_or({}))
dump_context(child_node_index, indent + 1);
};
for (auto const* root : root_contexts)
for (auto root : root_contexts)
dump_context(root, 1);
builder.append("\nDisplayList:\n"sv);
@ -7696,7 +7698,7 @@ String Document::dump_display_list()
builder.append_repeated(' ', indent * 2);
item.command.visit([&](auto const& command) {
builder.appendff("{}@{}", command.command_name, item.context ? item.context->id() : 0);
builder.appendff("{}@{}", command.command_name, item.context_index.value());
command.dump(builder);
});
builder.append('\n');

View file

@ -2587,7 +2587,7 @@ CSSPixelPoint Navigable::to_top_level_position(CSSPixelPoint a_position)
return {};
if (auto const* paintable_box = as_if<Painting::PaintableBox>(*paintable);
paintable_box && paintable_box->accumulated_visual_context()) {
paintable_box && paintable_box->accumulated_visual_context_index().value()) {
auto point = paintable_box->absolute_position();
point.translate_by(position);
position = paintable_box->transform_rect_to_viewport({ point, { 0, 0 } }).location();

View file

@ -44,7 +44,7 @@ static Optional<CSSPixelRect> scrollport_rect_in_viewport(Painting::PaintableBox
if (paintable_box.is_viewport_paintable())
return scrollport;
if (!paintable_box.accumulated_visual_context())
if (!paintable_box.accumulated_visual_context_index().value())
return {};
return paintable_box.transform_rect_to_viewport(scrollport);
}

View file

@ -12,28 +12,76 @@
namespace Web::Painting {
NonnullRefPtr<AccumulatedVisualContext> AccumulatedVisualContext::create(size_t id, VisualContextData data, RefPtr<AccumulatedVisualContext const> parent)
{
return adopt_ref(*new AccumulatedVisualContext(id, move(data), move(parent)));
}
bool ClipData::contains(DevicePixelPoint point) const
{
return corner_radii.contains(point.to_type<int>(), rect.to_type<int>());
}
Optional<Gfx::FloatPoint> AccumulatedVisualContext::transform_point_for_hit_test(Gfx::FloatPoint screen_point, ScrollStateSnapshot const& scroll_state) const
NonnullRefPtr<AccumulatedVisualContextTree> AccumulatedVisualContextTree::create()
{
Vector<AccumulatedVisualContext const*, 8> chain;
chain.ensure_capacity(m_depth);
for (auto const* node = this; node; node = node->parent().ptr())
chain.append(node);
auto visual_context_tree = adopt_ref(*new AccumulatedVisualContextTree());
// Sentinel at index 0 (null context). Data type doesn't matter; it's never accessed.
visual_context_tree->m_nodes.append({ ScrollData { 0, false }, {}, 0, false });
return visual_context_tree;
}
VisualContextIndex AccumulatedVisualContextTree::append(VisualContextData data, VisualContextIndex parent_index)
{
size_t depth = parent_index.value() ? m_nodes[parent_index.value()].depth + 1 : 1;
bool empty_clip = false;
if (parent_index.value() && m_nodes[parent_index.value()].has_empty_effective_clip) {
empty_clip = true;
} else if (data.has<ClipData>()) {
empty_clip = data.get<ClipData>().rect.is_empty();
} else if (data.has<ClipPathData>()) {
empty_clip = data.get<ClipPathData>().path.bounding_box().is_empty();
}
auto index = VisualContextIndex(m_nodes.size());
m_nodes.append({ move(data), parent_index, depth, empty_clip });
return index;
}
VisualContextIndex AccumulatedVisualContextTree::find_common_ancestor(VisualContextIndex a, VisualContextIndex b) const
{
if (!a.value() || !b.value())
return {};
size_t a_index = a.value();
size_t b_index = b.value();
while (m_nodes[a_index].depth > m_nodes[b_index].depth)
a_index = m_nodes[a_index].parent_index.value();
while (m_nodes[b_index].depth > m_nodes[a_index].depth)
b_index = m_nodes[b_index].parent_index.value();
while (a_index != b_index) {
a_index = m_nodes[a_index].parent_index.value();
b_index = m_nodes[b_index].parent_index.value();
}
return VisualContextIndex(a_index);
}
Vector<size_t, 8> AccumulatedVisualContextTree::build_ancestor_chain(VisualContextIndex index) const
{
auto const& node = m_nodes[index.value()];
Vector<size_t, 8> chain;
chain.ensure_capacity(node.depth);
for (size_t i = index.value(); i; i = m_nodes[i].parent_index.value())
chain.append(i);
return chain;
}
Optional<Gfx::FloatPoint> AccumulatedVisualContextTree::transform_point_for_hit_test(VisualContextIndex index, Gfx::FloatPoint screen_point, ScrollStateSnapshot const& scroll_state) const
{
if (!index.value())
return screen_point;
auto chain = build_ancestor_chain(index);
auto point = screen_point;
for (size_t i = chain.size(); i > 0; --i) {
auto const* node = chain[i - 1];
auto const& node = m_nodes[chain[i - 1]];
auto result = node->data().visit(
auto result = node.data.visit(
[&](PerspectiveData const& perspective) -> Optional<Gfx::FloatPoint> {
auto affine = Gfx::extract_2d_affine_transform(perspective.matrix);
auto inverse = affine.inverse();
@ -85,18 +133,18 @@ Optional<Gfx::FloatPoint> AccumulatedVisualContext::transform_point_for_hit_test
return point;
}
Gfx::FloatPoint AccumulatedVisualContext::inverse_transform_point(Gfx::FloatPoint screen_point) const
Gfx::FloatPoint AccumulatedVisualContextTree::inverse_transform_point(VisualContextIndex index, Gfx::FloatPoint screen_point) const
{
Vector<AccumulatedVisualContext const*, 8> chain;
chain.ensure_capacity(m_depth);
for (auto const* node = this; node; node = node->parent().ptr())
chain.append(node);
if (!index.value())
return screen_point;
auto chain = build_ancestor_chain(index);
auto point = screen_point;
for (size_t i = chain.size(); i > 0; --i) {
auto const* node = chain[i - 1];
auto const& node = m_nodes[chain[i - 1]];
node->data().visit(
node.data.visit(
[&](PerspectiveData const& perspective) {
auto affine = Gfx::extract_2d_affine_transform(perspective.matrix);
auto inverse = affine.inverse();
@ -118,11 +166,15 @@ Gfx::FloatPoint AccumulatedVisualContext::inverse_transform_point(Gfx::FloatPoin
return point;
}
Gfx::FloatRect AccumulatedVisualContext::transform_rect_to_viewport(Gfx::FloatRect const& source_rect, ScrollStateSnapshot const& scroll_state) const
Gfx::FloatRect AccumulatedVisualContextTree::transform_rect_to_viewport(VisualContextIndex index, Gfx::FloatRect const& source_rect, ScrollStateSnapshot const& scroll_state) const
{
if (!index.value())
return source_rect;
auto rect = source_rect;
for (auto const* node = this; node; node = node->parent().ptr()) {
node->data().visit(
for (size_t i = index.value(); i; i = m_nodes[i].parent_index.value()) {
auto const& node = m_nodes[i];
node.data.visit(
[&](TransformData const& transform) {
auto affine = Gfx::extract_2d_affine_transform(transform.matrix);
rect.translate_by(-transform.origin);
@ -144,9 +196,13 @@ Gfx::FloatRect AccumulatedVisualContext::transform_rect_to_viewport(Gfx::FloatRe
return rect;
}
void AccumulatedVisualContext::dump(StringBuilder& builder) const
void AccumulatedVisualContextTree::dump(VisualContextIndex index, StringBuilder& builder) const
{
m_data.visit(
if (!index.value())
return;
auto const& node = m_nodes[index.value()];
node.data.visit(
[&](PerspectiveData const&) {
builder.append("perspective"sv);
},

View file

@ -7,7 +7,9 @@
#pragma once
#include <AK/AtomicRefCounted.h>
#include <AK/DistinctNumeric.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/Filter.h>
#include <LibGfx/Matrix4x4.h>
@ -22,6 +24,8 @@ namespace Web::Painting {
class ScrollStateSnapshot;
AK_TYPEDEF_DISTINCT_ORDERED_ID(size_t, VisualContextIndex);
struct ScrollData {
size_t scroll_frame_id;
bool is_sticky;
@ -70,46 +74,36 @@ struct EffectsData {
using VisualContextData = Variant<ScrollData, ClipData, TransformData, PerspectiveData, ClipPathData, EffectsData>;
class AccumulatedVisualContext : public AtomicRefCounted<AccumulatedVisualContext> {
struct AccumulatedVisualContextNode {
VisualContextData data;
VisualContextIndex parent_index {};
size_t depth { 0 };
bool has_empty_effective_clip { false };
};
class AccumulatedVisualContextTree : public AtomicRefCounted<AccumulatedVisualContextTree> {
public:
static NonnullRefPtr<AccumulatedVisualContext> create(size_t id, VisualContextData data, RefPtr<AccumulatedVisualContext const> parent);
static NonnullRefPtr<AccumulatedVisualContextTree> create();
VisualContextData const& data() const { return m_data; }
RefPtr<AccumulatedVisualContext const> parent() const { return m_parent; }
VisualContextIndex append(VisualContextData data, VisualContextIndex parent_index);
bool is_effect() const { return m_data.has<EffectsData>(); }
bool has_empty_effective_clip() const { return m_has_empty_effective_clip; }
AccumulatedVisualContextNode const& node_at(VisualContextIndex index) const { return m_nodes[index.value()]; }
size_t depth() const { return m_depth; }
size_t id() const { return m_id; }
VisualContextIndex find_common_ancestor(VisualContextIndex a, VisualContextIndex b) const;
Optional<Gfx::FloatPoint> transform_point_for_hit_test(VisualContextIndex, Gfx::FloatPoint, ScrollStateSnapshot const&) const;
Gfx::FloatPoint inverse_transform_point(VisualContextIndex, Gfx::FloatPoint) const;
Gfx::FloatRect transform_rect_to_viewport(VisualContextIndex, Gfx::FloatRect const&, ScrollStateSnapshot const&) const;
void dump(VisualContextIndex, StringBuilder&) const;
void dump(StringBuilder&) const;
Optional<Gfx::FloatPoint> transform_point_for_hit_test(Gfx::FloatPoint, ScrollStateSnapshot const&) const;
Gfx::FloatPoint inverse_transform_point(Gfx::FloatPoint) const;
Gfx::FloatRect transform_rect_to_viewport(Gfx::FloatRect const&, ScrollStateSnapshot const&) const;
bool is_effect(VisualContextIndex i) const { return m_nodes[i.value()].data.has<EffectsData>(); }
bool has_empty_effective_clip(VisualContextIndex i) const { return m_nodes[i.value()].has_empty_effective_clip; }
private:
AccumulatedVisualContext(size_t id, VisualContextData data, RefPtr<AccumulatedVisualContext const> parent)
: m_data(move(data))
, m_parent(move(parent))
, m_depth(m_parent ? m_parent->depth() + 1 : 1)
, m_id(id)
{
if (m_parent && m_parent->has_empty_effective_clip()) {
m_has_empty_effective_clip = true;
} else if (m_data.has<ClipData>()) {
m_has_empty_effective_clip = m_data.get<ClipData>().rect.is_empty();
} else if (m_data.has<ClipPathData>()) {
m_has_empty_effective_clip = m_data.get<ClipPathData>().path.bounding_box().is_empty();
}
}
AccumulatedVisualContextTree() = default;
VisualContextData m_data;
RefPtr<AccumulatedVisualContext const> m_parent;
size_t m_depth;
size_t m_id;
bool m_has_empty_effective_clip { false };
Vector<size_t, 8> build_ancestor_chain(VisualContextIndex index) const;
Vector<AccumulatedVisualContextNode> m_nodes;
};
}

View file

@ -21,7 +21,7 @@ namespace Web::Painting {
static RefPtr<DisplayList> compute_text_clip_paths(DisplayListRecordingContext& context, Paintable const& paintable, CSSPixelPoint containing_block_location)
{
auto text_clip_paths = DisplayList::create();
auto text_clip_paths = DisplayList::create(AccumulatedVisualContextTree::create());
DisplayListRecorder display_list_recorder(*text_clip_paths);
// Remove containing block offset, so executing the display list will produce mask at (0, 0)
display_list_recorder.translate(-context.floored_device_point(containing_block_location).to_type<int>());

View file

@ -10,11 +10,11 @@
namespace Web::Painting {
bool DisplayList::append(DisplayListCommand&& command, RefPtr<AccumulatedVisualContext const> context)
bool DisplayList::append(DisplayListCommand&& command, VisualContextIndex context_index)
{
if (context && context->has_empty_effective_clip())
if (context_index.value() && m_visual_context_tree->has_empty_effective_clip(context_index))
return false;
m_commands.append({ move(context), move(command) });
m_commands.append({ context_index, move(command) });
return true;
}
@ -64,38 +64,22 @@ void DisplayListPlayer::execute_display_list_into_surface(DisplayList& display_l
execute_impl(display_list, scroll_state_snapshot);
}
static RefPtr<AccumulatedVisualContext const> find_common_ancestor(RefPtr<AccumulatedVisualContext const> a, RefPtr<AccumulatedVisualContext const> b)
{
if (!a || !b)
return {};
while (a->depth() > b->depth())
a = a->parent();
while (b->depth() > a->depth())
b = b->parent();
while (a != b) {
a = a->parent();
b = b->parent();
}
return a;
}
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, RefPtr<AccumulatedVisualContext const> common_ancestor, RefPtr<AccumulatedVisualContext const> node, auto&& callback) -> void {
if (!node || node == common_ancestor)
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, node->parent(), callback);
callback(*node);
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 = [&](AccumulatedVisualContext const& node) {
node.data().visit(
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 });
},
@ -126,31 +110,31 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
});
};
RefPtr<AccumulatedVisualContext const> applied_context;
VisualContextIndex applied_context_index;
size_t applied_depth = 0;
auto switch_to_context = [&](RefPtr<AccumulatedVisualContext const> const& target_context) {
if (applied_context == target_context)
auto switch_to_context = [&](VisualContextIndex target_index) {
if (applied_context_index == target_index)
return;
auto common_ancestor = find_common_ancestor(applied_context, target_context);
auto common_ancestor_depth = common_ancestor ? common_ancestor->depth() : 0;
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, target_context, [&](AccumulatedVisualContext const& node) {
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 = target_context;
applied_context_index = target_index;
};
for (size_t command_index = 0; command_index < commands.size(); command_index++) {
auto const& [context, command] = commands[command_index];
auto const& [context_index, command] = commands[command_index];
auto bounding_rect = command_bounding_rectangle(command);
@ -161,13 +145,13 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
// 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 && applied_context != context && context->is_effect() && bounding_rect.has_value()) {
switch_to_context(context->parent());
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);
switch_to_context(context_index);
if (command.has<PaintScrollBar>()) {
auto translated_command = command;

View file

@ -73,24 +73,30 @@ private:
class DisplayList : public AtomicRefCounted<DisplayList> {
public:
static NonnullRefPtr<DisplayList> create()
static NonnullRefPtr<DisplayList> create(NonnullRefPtr<AccumulatedVisualContextTree const> visual_context_tree)
{
return adopt_ref(*new DisplayList());
return adopt_ref(*new DisplayList(move(visual_context_tree)));
}
bool append(DisplayListCommand&& command, RefPtr<AccumulatedVisualContext const> context);
bool append(DisplayListCommand&& command, VisualContextIndex context_index);
struct CommandListItem {
RefPtr<AccumulatedVisualContext const> context;
VisualContextIndex context_index {};
DisplayListCommand command;
};
AccumulatedVisualContextTree const& visual_context_tree() const { return *m_visual_context_tree; }
auto& commands(Badge<DisplayListRecorder>) { return m_commands; }
auto const& commands() const { return m_commands; }
private:
DisplayList() = default;
explicit DisplayList(NonnullRefPtr<AccumulatedVisualContextTree const> visual_context_tree)
: m_visual_context_tree(move(visual_context_tree))
{
}
NonnullRefPtr<AccumulatedVisualContextTree const> const m_visual_context_tree;
AK::SegmentedVector<CommandListItem, 512> m_commands;
};

View file

@ -60,17 +60,17 @@ consteval static int command_nesting_level_change(T const& command)
return 0;
}
#define APPEND(...) \
do { \
auto command = __VA_ARGS__; \
m_save_nesting_level += command_nesting_level_change(command); \
if (m_is_capturing) { \
auto command_copy = command; \
if (m_display_list.append(move(command), m_accumulated_visual_context)) \
m_captured_commands.append(move(command_copy)); \
} else { \
m_display_list.append(move(command), m_accumulated_visual_context); \
} \
#define APPEND(...) \
do { \
auto command = __VA_ARGS__; \
m_save_nesting_level += command_nesting_level_change(command); \
if (m_is_capturing) { \
auto command_copy = command; \
if (m_display_list.append(move(command), m_accumulated_visual_context_index)) \
m_captured_commands.append(move(command_copy)); \
} else { \
m_display_list.append(move(command), m_accumulated_visual_context_index); \
} \
} while (false)
void DisplayListRecorder::replay_cached_commands(ReadonlySpan<DisplayListCommand> commands)
@ -82,7 +82,7 @@ void DisplayListRecorder::replay_cached_commands(ReadonlySpan<DisplayListCommand
return command.nesting_level_change;
return 0;
});
m_display_list.append(move(command_copy), m_accumulated_visual_context);
m_display_list.append(move(command_copy), m_accumulated_visual_context_index);
}
}

View file

@ -87,8 +87,8 @@ public:
void translate(Gfx::IntPoint delta);
void set_accumulated_visual_context(RefPtr<AccumulatedVisualContext const> state) { m_accumulated_visual_context = move(state); }
RefPtr<AccumulatedVisualContext const> accumulated_visual_context() const { return m_accumulated_visual_context; }
void set_accumulated_visual_context(VisualContextIndex index) { m_accumulated_visual_context_index = index; }
VisualContextIndex accumulated_visual_context() const { return m_accumulated_visual_context_index; }
void replay_cached_commands(ReadonlySpan<DisplayListCommand> commands);
@ -149,7 +149,7 @@ public:
private:
void end_capture();
RefPtr<AccumulatedVisualContext const> m_accumulated_visual_context;
VisualContextIndex m_accumulated_visual_context_index {};
Vector<size_t> m_push_sc_index_stack;
DisplayList& m_display_list;
bool m_is_capturing { false };

View file

@ -19,6 +19,7 @@
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/PaintableWithLines.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/Painting/ViewportPaintable.h>
namespace Web::Painting {
@ -169,27 +170,30 @@ void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) co
paintable_box = first_ancestor_of_type<PaintableBox>();
if (paintable_box) {
Vector<RefPtr<AccumulatedVisualContext const>> relevant_contexts;
for (auto visual_context = paintable_box->accumulated_visual_context(); visual_context != nullptr; visual_context = visual_context->parent()) {
auto should_keep_entry = visual_context->data().visit(
[](ScrollData const&) -> bool { return true; },
[](ClipData const&) -> bool { return false; },
[](TransformData const&) -> bool { return true; },
[](PerspectiveData const&) -> bool { return true; },
[](ClipPathData const&) -> bool { return false; },
[](EffectsData const&) -> bool { return false; });
auto* visual_context_tree = const_cast<ViewportPaintable*>(document().paintable())->visual_context_tree();
auto visual_context_index = paintable_box->accumulated_visual_context_index();
if (should_keep_entry)
relevant_contexts.append(visual_context);
if (visual_context_tree && visual_context_index.value()) {
Vector<VisualContextIndex> relevant_indices;
for (auto i = visual_context_index; i.value(); i = visual_context_tree->node_at(i).parent_index) {
auto should_keep = visual_context_tree->node_at(i).data.visit(
[](ScrollData const&) { return true; },
[](ClipData const&) { return false; },
[](TransformData const&) { return true; },
[](PerspectiveData const&) { return true; },
[](ClipPathData const&) { return false; },
[](EffectsData const&) { return false; });
if (should_keep)
relevant_indices.append(i);
}
VisualContextIndex overlay_visual_context_index {};
for (auto const& source_visual_context_index : relevant_indices.in_reverse())
overlay_visual_context_index = visual_context_tree->append(visual_context_tree->node_at(source_visual_context_index).data, overlay_visual_context_index);
if (overlay_visual_context_index.value())
display_list_recorder.set_accumulated_visual_context(overlay_visual_context_index);
}
auto visual_context_id = 1;
RefPtr<AccumulatedVisualContext> copied_visual_context;
for (auto const& original_visual_context : relevant_contexts.in_reverse())
copied_visual_context = AccumulatedVisualContext::create(visual_context_id++, original_visual_context->data(), copied_visual_context);
if (copied_visual_context)
display_list_recorder.set_accumulated_visual_context(copied_visual_context);
}
paint_inspector_overlay_internal(context);

View file

@ -166,8 +166,8 @@ void PaintableBox::reset_for_relayout()
m_enclosing_scroll_frame = nullptr;
m_own_scroll_frame = nullptr;
m_accumulated_visual_context = nullptr;
m_accumulated_visual_context_for_descendants = nullptr;
m_accumulated_visual_context_index = {};
m_accumulated_visual_context_for_descendants_index = {};
m_used_values_for_grid_template_columns = nullptr;
m_used_values_for_grid_template_rows = nullptr;
@ -890,11 +890,12 @@ Optional<int> PaintableBox::scroll_frame_id() const
Optional<CSSPixelPoint> PaintableBox::transform_point_to_local(CSSPixelPoint screen_position) const
{
if (!accumulated_visual_context())
if (!m_accumulated_visual_context_index.value())
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto result = accumulated_visual_context()->transform_point_for_hit_test(screen_position.to_type<float>() * pixel_ratio, scroll_state);
auto const& visual_context_tree = *document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio, scroll_state);
if (!result.has_value())
return {};
return (*result / pixel_ratio).to_type<CSSPixels>();
@ -902,11 +903,12 @@ Optional<CSSPixelPoint> PaintableBox::transform_point_to_local(CSSPixelPoint scr
Optional<CSSPixelPoint> PaintableBox::transform_point_to_local_for_descendants(CSSPixelPoint screen_position) const
{
if (!accumulated_visual_context_for_descendants())
if (!m_accumulated_visual_context_for_descendants_index.value())
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto result = accumulated_visual_context_for_descendants()->transform_point_for_hit_test(screen_position.to_type<float>() * pixel_ratio, scroll_state);
auto const& visual_context_tree = *document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_point_for_hit_test(m_accumulated_visual_context_for_descendants_index, screen_position.to_type<float>() * pixel_ratio, scroll_state);
if (!result.has_value())
return {};
return (*result / pixel_ratio).to_type<CSSPixels>();
@ -914,20 +916,22 @@ Optional<CSSPixelPoint> PaintableBox::transform_point_to_local_for_descendants(C
CSSPixelRect PaintableBox::transform_rect_to_viewport(CSSPixelRect const& rect) const
{
if (!accumulated_visual_context())
if (!m_accumulated_visual_context_index.value())
return rect;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto const& scroll_state = document().paintable()->scroll_state_snapshot();
auto result = accumulated_visual_context()->transform_rect_to_viewport(rect.to_type<float>() * pixel_ratio, scroll_state);
auto const& visual_context_tree = *document().paintable()->visual_context_tree();
auto result = visual_context_tree.transform_rect_to_viewport(m_accumulated_visual_context_index, rect.to_type<float>() * pixel_ratio, scroll_state);
return (result * (1.f / pixel_ratio)).to_type<CSSPixels>();
}
CSSPixelPoint PaintableBox::inverse_transform_point(CSSPixelPoint screen_position) const
{
if (!accumulated_visual_context())
if (!m_accumulated_visual_context_index.value())
return screen_position;
auto pixel_ratio = static_cast<float>(document().page().client().device_pixels_per_css_pixel());
auto result = accumulated_visual_context()->inverse_transform_point(screen_position.to_type<float>() * pixel_ratio);
auto const& visual_context_tree = *document().paintable()->visual_context_tree();
auto result = visual_context_tree.inverse_transform_point(m_accumulated_visual_context_index, screen_position.to_type<float>() * pixel_ratio);
return (result / pixel_ratio).to_type<CSSPixels>();
}

View file

@ -230,10 +230,10 @@ public:
void set_enclosing_scroll_frame(RefPtr<ScrollFrame const> const& scroll_frame) { m_enclosing_scroll_frame = scroll_frame; }
void set_own_scroll_frame(RefPtr<ScrollFrame> const& scroll_frame) { m_own_scroll_frame = scroll_frame; }
void set_accumulated_visual_context(auto state) { m_accumulated_visual_context = move(state); }
[[nodiscard]] auto accumulated_visual_context() const { return m_accumulated_visual_context; }
void set_accumulated_visual_context_for_descendants(auto state) { m_accumulated_visual_context_for_descendants = move(state); }
[[nodiscard]] auto accumulated_visual_context_for_descendants() const { return m_accumulated_visual_context_for_descendants; }
void set_accumulated_visual_context(VisualContextIndex index) { m_accumulated_visual_context_index = index; }
[[nodiscard]] VisualContextIndex accumulated_visual_context_index() const { return m_accumulated_visual_context_index; }
void set_accumulated_visual_context_for_descendants(VisualContextIndex index) { m_accumulated_visual_context_for_descendants_index = index; }
[[nodiscard]] VisualContextIndex accumulated_visual_context_for_descendants_index() const { return m_accumulated_visual_context_for_descendants_index; }
Optional<CSSPixelPoint> transform_point_to_local(CSSPixelPoint screen_position) const;
Optional<CSSPixelPoint> transform_point_to_local_for_descendants(CSSPixelPoint screen_position) const;
@ -329,8 +329,8 @@ private:
RefPtr<ScrollFrame const> m_enclosing_scroll_frame;
RefPtr<ScrollFrame const> m_own_scroll_frame;
RefPtr<AccumulatedVisualContext const> m_accumulated_visual_context;
RefPtr<AccumulatedVisualContext const> m_accumulated_visual_context_for_descendants;
VisualContextIndex m_accumulated_visual_context_index {};
VisualContextIndex m_accumulated_visual_context_for_descendants_index {};
Optional<BordersDataWithElementKind> m_override_borders_data;
Optional<TableCellCoordinates> m_table_cell_coordinates;

View file

@ -77,7 +77,7 @@ static RefPtr<DisplayList> paint_mask_or_clip_to_display_list(
bool is_clip_path)
{
auto mask_rect = context.enclosing_device_rect(area);
auto display_list = DisplayList::create();
auto display_list = DisplayList::create(AccumulatedVisualContextTree::create());
DisplayListRecorder display_list_recorder(*display_list);
display_list_recorder.translate(-mask_rect.location().to_type<int>());
auto paint_context = context.clone(display_list_recorder);

View file

@ -25,7 +25,7 @@ SVGSVGPaintable::SVGSVGPaintable(Layout::SVGSVGBox const& layout_box)
void SVGSVGPaintable::paint_svg_box(DisplayListRecordingContext& context, PaintableBox const& svg_box, PaintPhase phase)
{
context.display_list_recorder().set_accumulated_visual_context(svg_box.accumulated_visual_context());
context.display_list_recorder().set_accumulated_visual_context(svg_box.accumulated_visual_context_index());
// For elements with SVG filters, emit a transparent FillRect to trigger filter application.
// This ensures content-generating filters (feFlood, feImage) work even with empty source.

View file

@ -37,9 +37,9 @@ static void paint_node(Paintable const& paintable, DisplayListRecordingContext&
// Text fragments in a PaintableWithLines are content of the block container.
// They need the descendants' visual context, not the element's own visual context.
if (is<PaintableWithLines>(paintable) && phase == PaintPhase::Foreground)
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context_for_descendants());
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context_for_descendants_index());
else
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context());
context.display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context_index());
}
bool const skip_cache = !paintable_box || context.should_show_line_box_borders();
@ -323,8 +323,8 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
mask_image->resolve_for_size(paintable_box().layout_node_with_style_and_box_metrics(), paintable_box().absolute_padding_box_rect().size());
}
auto effective_state = paintable_box().accumulated_visual_context();
context.display_list_recorder().set_accumulated_visual_context(effective_state);
auto effective_context_index = paintable_box().accumulated_visual_context_index();
context.display_list_recorder().set_accumulated_visual_context(effective_context_index);
// For elements with SVG filters, emit a transparent FillRect to trigger filter application.
// This ensures content-generating filters (feFlood, feImage) work even with empty source.
@ -337,7 +337,7 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
Vector<DisplayListRecorder::MaskInfo> masks;
if (mask_image) {
auto mask_display_list = DisplayList::create();
auto mask_display_list = DisplayList::create(AccumulatedVisualContextTree::create());
DisplayListRecorder display_list_recorder(*mask_display_list);
auto mask_painting_context = context.clone(display_list_recorder);
auto mask_rect_in_device_pixels = context.enclosing_device_rect(paintable_box().absolute_padding_box_rect());

View file

@ -45,7 +45,8 @@ void ViewportPaintable::reset_for_relayout()
m_scroll_state_snapshot = {};
m_needs_to_refresh_scroll_state = true;
m_paintable_boxes_with_auto_content_visibility.clear();
m_next_accumulated_visual_context_id = 1;
m_visual_context_tree = nullptr;
m_visual_viewport_context_index = {};
}
void ViewportPaintable::build_stacking_context_tree_if_needed()
@ -303,14 +304,14 @@ static Optional<ClipData> compute_clip_data(PaintableBox const& paintable_box, C
void ViewportPaintable::assign_accumulated_visual_contexts()
{
m_next_accumulated_visual_context_id = 1;
m_visual_context_tree = AccumulatedVisualContextTree::create();
auto pixel_ratio = document().page().client().device_pixels_per_css_pixel();
DevicePixelConverter converter { pixel_ratio };
auto scale = static_cast<float>(pixel_ratio);
auto append_node = [&](RefPtr<AccumulatedVisualContext const> parent, VisualContextData data) {
return AccumulatedVisualContext::create(allocate_accumulated_visual_context_id(), move(data), parent);
auto append_node = [&](VisualContextIndex parent_index, VisualContextData data) -> VisualContextIndex {
return m_visual_context_tree->append(move(data), parent_index);
};
auto make_effects_data = [&](PaintableBox const& box) -> Optional<EffectsData> {
@ -327,17 +328,17 @@ void ViewportPaintable::assign_accumulated_visual_contexts()
};
// Create visual viewport transform as root (if not identity)
m_visual_viewport_context = nullptr;
m_visual_viewport_context_index = {};
auto transform = document().visual_viewport()->transform();
if (!transform.is_identity()) {
auto matrix = scale_matrix_for_device_pixels(transform.to_matrix(), scale);
m_visual_viewport_context = append_node(nullptr, TransformData { matrix, { 0.f, 0.f } });
m_visual_viewport_context_index = append_node({}, TransformData { matrix, { 0.f, 0.f } });
}
RefPtr<AccumulatedVisualContext const> viewport_state_for_descendants = m_visual_viewport_context;
VisualContextIndex viewport_state_for_descendants = m_visual_viewport_context_index;
if (own_scroll_frame())
viewport_state_for_descendants = append_node(m_visual_viewport_context, ScrollData { own_scroll_frame()->id(), false });
set_accumulated_visual_context(nullptr);
viewport_state_for_descendants = append_node(m_visual_viewport_context_index, ScrollData { own_scroll_frame()->id(), false });
set_accumulated_visual_context({});
set_accumulated_visual_context_for_descendants(viewport_state_for_descendants);
for_each_in_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
@ -352,14 +353,14 @@ void ViewportPaintable::assign_accumulated_visual_contexts()
else
paintable_box.set_filter({});
RefPtr<AccumulatedVisualContext const> inherited_state;
VisualContextIndex inherited_state;
if (paintable_box.is_fixed_position()) {
inherited_state = m_visual_viewport_context;
inherited_state = m_visual_viewport_context_index;
} else if (paintable_box.is_absolutely_positioned()) {
// For position: absolute, use containing block's state to correctly escape scroll containers.
auto* containing = paintable_box.containing_block();
inherited_state = containing->accumulated_visual_context_for_descendants();
inherited_state = containing->accumulated_visual_context_for_descendants_index();
// Abspos elements escape scroll containers and overflow clips of non-positioned
// ancestors, but cannot escape stacking contexts created by intermediate effects
@ -381,11 +382,11 @@ void ViewportPaintable::assign_accumulated_visual_contexts()
// For position: relative/static, use visual parent's state directly.
// This avoids duplicate transform/perspective allocations that would occur with
// the containing block + intermediate walk approach.
inherited_state = visual_parent->accumulated_visual_context_for_descendants();
inherited_state = visual_parent->accumulated_visual_context_for_descendants_index();
}
// Build this element's own state from inherited state.
RefPtr<AccumulatedVisualContext const> own_state = inherited_state;
VisualContextIndex own_state = inherited_state;
if (paintable_box.is_sticky_position()) {
// For sticky elements, use enclosing_scroll_frame which holds the sticky frame.
@ -430,7 +431,7 @@ void ViewportPaintable::assign_accumulated_visual_contexts()
paintable_box.set_accumulated_visual_context(own_state);
// Build state for descendants: own state + perspective + clip + scroll.
RefPtr<AccumulatedVisualContext const> state_for_descendants = own_state;
VisualContextIndex state_for_descendants = own_state;
if (auto perspective_matrix = compute_perspective_matrix(paintable_box, computed_values); perspective_matrix.has_value()) {
auto scaled_matrix = scale_matrix_for_device_pixels(*perspective_matrix, scale);

View file

@ -44,7 +44,8 @@ public:
void set_paintable_boxes_with_auto_content_visibility(Vector<GC::Ref<PaintableBox>> paintable_boxes) { m_paintable_boxes_with_auto_content_visibility = move(paintable_boxes); }
ReadonlySpan<GC::Ref<PaintableBox>> paintable_boxes_with_auto_content_visibility() const { return m_paintable_boxes_with_auto_content_visibility; }
size_t allocate_accumulated_visual_context_id() { return m_next_accumulated_visual_context_id++; }
AccumulatedVisualContextTree const* visual_context_tree() const { return m_visual_context_tree.ptr(); }
AccumulatedVisualContextTree* visual_context_tree() { return m_visual_context_tree.ptr(); }
private:
virtual bool is_viewport_paintable() const override { return true; }
@ -61,9 +62,8 @@ private:
Vector<GC::Ref<PaintableBox>> m_paintable_boxes_with_auto_content_visibility;
size_t m_next_accumulated_visual_context_id { 1 };
RefPtr<AccumulatedVisualContext const> m_visual_viewport_context;
RefPtr<AccumulatedVisualContextTree> m_visual_context_tree;
VisualContextIndex m_visual_viewport_context_index {};
};
template<>

View file

@ -289,7 +289,7 @@ Optional<Painting::PaintStyle> SVGPatternElement::to_gfx_paint_style(SVGPaintCon
auto svg_offset = recording_context.rounded_device_point(svg_element_rect.location()).to_type<int>().to_type<float>();
tile_rect.translate_by(svg_offset);
auto display_list = Painting::DisplayList::create();
auto display_list = Painting::DisplayList::create(Painting::AccumulatedVisualContextTree::create());
Painting::DisplayListRecorder display_list_recorder(*display_list);
auto content_origin = paint_context.paint_transform.map(Gfx::FloatPoint { 0, 0 }) + svg_offset;
display_list_recorder.translate(-Gfx::IntPoint(content_origin.to_type<int>()));