This page covers the painting pipeline in LibWeb: how the paintable tree is built from the layout tree, how paint order is managed via stacking contexts, how drawing operations are recorded into a DisplayList, and how that list is played back through the Skia graphics backend on a dedicated rendering thread.
For the layout system that produces the input to this pipeline, see Layout System. For SVG filter integration and the graphics abstraction layer, see Canvas and Graphics Integration.
The painting pipeline converts a committed layout tree into pixels rendered to a backing surface. It is structured in three major stages:
Paintable::paint() call emits drawing commands into a DisplayList.DisplayListPlayerSkia on the RenderingThread executes the recorded commands against a Gfx::PaintingSurface using the Skia graphics library.Overall painting pipeline diagram:
Sources: Libraries/LibWeb/Painting/ViewportPaintable.cpp Libraries/LibWeb/HTML/Navigable.cpp Libraries/LibWeb/HTML/RenderingThread.cpp Libraries/LibWeb/Painting/DisplayList.cpp
Every node in the paintable tree is a Paintable. The hierarchy mirrors the layout tree hierarchy.
Sources: Libraries/LibWeb/Painting/Paintable.h Libraries/LibWeb/Painting/PaintableBox.h Libraries/LibWeb/Painting/ViewportPaintable.cpp Libraries/LibWeb/Painting/SVGSVGPaintable.cpp
Paintables are created by LayoutState::commit() (see Layout System). Each layout node creates exactly one Paintable via factory methods like PaintableBox::create(Layout::Box const&). After commit, the ViewportPaintable is the root of the new paintable tree.
Each node's paint() method is called once per phase in a defined sequence. The phases are declared in Paintable.h:
| Phase | What is drawn |
|---|---|
Background | Background color, images, box shadows, backdrop filters |
Border | Regular borders |
TableCollapsedBorder | Collapsed table borders |
Foreground | Text, replaced content, foreground images |
Outline | CSS outline |
Overlay | Scrollbars, resize handles, inspector overlays |
Sources: Libraries/LibWeb/Painting/Paintable.h22-29 Libraries/LibWeb/Painting/PaintableBox.cpp543-636
ViewportPaintable::build_stacking_context_tree() walks the paintable tree and allocates StackingContext objects for any PaintableBox where establishes_stacking_context() is true (elements with opacity < 1, transform, z-index on positioned elements, etc.).
Each StackingContext holds:
PaintableBoxm_children list of child stacking contexts (sorted by z-index)m_positioned_descendants_and_stacking_contexts_with_stack_level_0 — positioned items with z-index 0 or autom_non_positioned_floating_descendants — float elementsAfter construction, StackingContext::sort() sorts the children list by z-index, then by tree order for ties.
StackingContext::paint_internal() implements the CSS 2.1 Appendix E paint order:
paint_descendants() handles steps 4–7 by iterating the paintable tree and dispatching to paint_node(), which calls Paintable::paint() for each phase. Floats and inline replacements are routed through paint_node_as_stacking_context(), treating them as independent contexts.
For SVG roots (is_svg_svg_paintable()), painting is delegated to SVGSVGPaintable::paint_svg_box() instead.
Sources: Libraries/LibWeb/Painting/StackingContext.cpp211-281 Libraries/LibWeb/Painting/ViewportPaintable.cpp55-78
A DisplayList is a flat sequence of (RefPtr<AccumulatedVisualContext const>, DisplayListCommand) pairs.
DisplayListCommand is a Variant<> over all possible drawing operations, defined in DisplayListCommand.h. Major commands include:
| Command | Description |
|---|---|
FillRect | Filled rectangle |
DrawGlyphRun | Shaped text |
DrawScaledImmutableBitmap | Image drawing |
FillPath / StrokePath | Path rendering |
PaintLinearGradient, PaintRadialGradient, PaintConicGradient | CSS gradient fills |
PaintOuterBoxShadow, PaintInnerBoxShadow | Box shadows |
PaintTextShadow | Text shadow |
ApplyBackdropFilter | CSS backdrop-filter |
Save, SaveLayer, Restore | Painter state stack |
Translate, AddClipRect, AddRoundedRectClip | Transform/clip operations |
PaintScrollBar | Scrollbar chrome overlay |
ApplyEffects | Opacity, blend mode, filter |
PaintNestedDisplayList | Nested display list (for masks) |
Each command carries a bounding_rect() used for culling during playback.
Sources: Libraries/LibWeb/Painting/DisplayListCommand.h Libraries/LibWeb/Painting/DisplayList.h
DisplayListRecorder is the write interface to a DisplayList. It is constructed with a DisplayList& and provides one method per command type. Every emit operation uses the APPEND macro which tags the command with the current m_accumulated_visual_context:
Paintables access the recorder through a DisplayListRecordingContext object, which aggregates the recorder, device pixel converter, palette, and chrome metrics.
Sources: Libraries/LibWeb/Painting/DisplayListRecorder.h Libraries/LibWeb/Painting/DisplayListRecorder.cpp30-36
AccumulatedVisualContext encodes the cumulative visual transformation state at a point in the paintable tree. It forms a parent-linked tree, where each node holds one of:
| Variant | Purpose |
|---|---|
TransformData | CSS transform matrix + origin |
PerspectiveData | CSS perspective matrix |
ClipData | Overflow or explicit clip rect, with corner radii |
ClipPathData | Clip path (SVG/CSS) |
ScrollData | Scroll frame id (resolved to offset at playback time) |
EffectsData | Opacity, blend mode, CSS filter |
ViewportPaintable::assign_accumulated_visual_contexts() pre-computes these chains by walking the paintable tree, composing the chain from parent to child. Each PaintableBox stores two references:
m_accumulated_visual_context — the context node corresponding to the box itself (used to paint the box)m_accumulated_visual_context_for_descendants — the context that should apply to the box's childrenDuring display list recording, StackingContext::paint_node() calls display_list_recorder().set_accumulated_visual_context(paintable_box->accumulated_visual_context()) before each paintable's paint() call. This tags every command with the correct context chain.
Sources: Libraries/LibWeb/Painting/ViewportPaintable.cpp282-430 Libraries/LibWeb/Painting/StackingContext.cpp29-39 Libraries/LibWeb/Painting/DisplayListRecorder.h90-91
A ScrollFrame is allocated by ViewportPaintable::assign_scroll_frames() for each paintable with scrollable overflow and for each sticky-positioned element. Each frame has a unique integer id used to look up scroll offsets at playback time.
Sticky frames additionally store StickyConstraints computed by precompute_sticky_constraints() — the position relative to the scroll ancestor, the containing block region, and the CSS inset values.
assign_scroll_frames() also assigns m_enclosing_scroll_frame on each descendant, linking every non-fixed PaintableBox to the nearest ancestor scroll frame.
ViewportPaintable::m_scroll_state is the live ScrollState, holding per-frame offsets as they change with user interaction. Before each paint, a ScrollStateSnapshot is taken. This snapshot is passed to the RenderingThread along with the display list, ensuring the paint is consistent with the scroll position at a single point in time.
During display list playback in DisplayListPlayer::execute_impl(), ScrollData context nodes are resolved by calling scroll_state.own_offset_for_frame_with_id(scroll_frame_id) and translating the canvas accordingly.
PaintableBox::set_scroll_offset() updates the live scroll state, queues a scroll event, and calls set_needs_display() to trigger a repaint.
Sources: Libraries/LibWeb/Painting/ViewportPaintable.cpp88-160 Libraries/LibWeb/Painting/DisplayList.cpp120-130 Libraries/LibWeb/Painting/PaintableBox.cpp136-200
DisplayListPlayer is the abstract base class defining the playback interface. DisplayListPlayer::execute_impl() (in DisplayList.cpp) is the non-virtual core driver. It iterates the command list, calls switch_to_context() to reconcile the painter's save/restore stack with the target AccumulatedVisualContext, and dispatches each command to the virtual handler.
DisplayListPlayerSkia is the only concrete implementation. It translates each command into Skia API calls against an SkCanvas obtained from the active surface.
AccumulatedVisualContext transitions are driven by switch_to_context(). It finds the common ancestor of the current and target context nodes, restores up to that point, then descends to the target by calling apply_accumulated_visual_context() for each node. EffectsData nodes open a new layer via SaveLayer; TransformData, ClipData, and ScrollData nodes emit save/transform/clip.
Sources: Libraries/LibWeb/Painting/DisplayList.cpp42-230 Libraries/LibWeb/Painting/DisplayListPlayerSkia.h Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp
RenderingThread is owned by Navigable and runs DisplayListPlayerSkia on a dedicated background thread to keep GPU work off the main thread.
RenderingThread is initialized in the Navigable constructor. It holds a BackingStoreState with front and back Gfx::PaintingSurface references and their integer IDs:
Libraries/LibWeb/HTML/RenderingThread.cpp16-25
The Skia backend context is determined at Navigable construction time:
Gfx::get_metal_context() → SkiaBackendContext::create_metal_context()Gfx::create_vulkan_context() → SkiaBackendContext::create_vulkan_context()DisplayListPlayerSkia constructed without a context)Sources: Libraries/LibWeb/HTML/Navigable.cpp147-196 Libraries/LibWeb/HTML/RenderingThread.cpp Libraries/LibWeb/HTML/RenderingThread.h
Hit testing answers "which paintable is at this screen point?" It is used by EventHandler to route mouse events.
StackingContext::hit_test() traverses in reverse paint order (so the topmost-painted element wins):
m_positioned_descendants_and_stacking_contexts_with_stack_level_0PaintableBox::hit_test() accounts for accumulated_visual_context() when computing whether a point falls inside a transformed element, calling AccumulatedVisualContext::transform_point_for_hit_test() to map from screen space into local space.
HitTestResult carries the Paintable*, an index_in_node (byte offset for text), and optional distance fields used for "closest text cursor" (HitTestType::TextCursor) resolution.
EventHandler::target_for_mouse_position() calls paint_root()->hit_test(visual_viewport_position, ...) to find the top paintable, then dispatches DOM events through the identified node.
Sources: Libraries/LibWeb/Painting/StackingContext.cpp350-440 Libraries/LibWeb/Painting/PaintableBox.cpp778-787 Libraries/LibWeb/Page/EventHandler.cpp543-555
SVG has a separate paint path that bypasses the normal stacking context phase loop.
SVGSVGPaintable overrides StackingContext::paint_internal() handling: when the stacking context root is_svg_svg_paintable(), painting is routed to SVGSVGPaintable::paint_descendants() which recurses through the SVG element tree calling paint_node() with PaintPhase::Foreground on each child.
SVGPathPaintable::paint() emits FillPath and StrokePath commands with SVG-specific fill/stroke attributes (including currentColor, url() paint servers, and marker handling).
SVG filters are handled at the stacking context level: when a PaintableBox has an SVG filter (filter().svg_filter_bounds), a transparent FillRect is emitted first to establish a layer, and the filter is applied via AccumulatedVisualContext EffectsData.
SVGSVGPaintable::paint_svg_box() clips the SVG viewport and delegates to the child paint loop, enabling embedded SVG in HTML to participate correctly in the stacking context tree.
Sources: Libraries/LibWeb/Painting/SVGSVGPaintable.cpp Libraries/LibWeb/Painting/SVGPathPaintable.cpp Libraries/LibWeb/Painting/StackingContext.cpp100-125 Libraries/LibWeb/Painting/StackingContext.cpp305-312
Sources: Libraries/LibWeb/Painting/ViewportPaintable.cpp Libraries/LibWeb/Painting/StackingContext.cpp Libraries/LibWeb/HTML/RenderingThread.cpp Libraries/LibWeb/Painting/DisplayList.cpp
Refresh this wiki