How Ladybird Composites Web Content: The Painting Pipeline Explained

Ladybird converts layout trees into Paintable objects, builds stacking contexts to handle z-index and effects, records drawing commands into a display list, and rasterizes them via Skia to composite web content onto the screen.

The Ladybird browser engine transforms DOM and CSS into pixels through a multi-stage painting process that bridges the layout engine's geometric calculations with GPU-accelerated rendering. This pipeline handles everything from simple backgrounds to complex blend modes, backdrop filters, and scrollable containers, all orchestrated through the Libraries/LibWeb/Painting module in the LadybirdBrowser/ladybird repository.

From Layout to Paintable Objects

The painting process begins after the layout engine constructs the layout tree (Layout::Node hierarchy). Each layout node that requires rasterization is converted into a Paintable object—concrete subclasses include PaintableBox, TextPaintable, and SVGPaintable.

The root of this hierarchy is the ViewportPaintable, a special PaintableWithLines defined in Libraries/LibWeb/Painting/ViewportPaintable.h that represents the entire viewport. This object serves as the entry point for all subsequent painting operations.

Building Stacking Contexts

Before recording any drawing commands, the engine establishes the z-order and effect scoping hierarchy. The ViewportPaintable::build_stacking_context_tree_if_needed() method walks the paintable tree and constructs a hierarchy of StackingContext objects.

These stacking contexts, defined in Libraries/LibWeb/Painting/StackingContext.h, capture CSS properties including z-index, opacity, mix-blend-mode, and isolation. They define the precise order in which descendants are composited, ensuring that visual effects are applied in the correct scope.

Scroll Frames and Visual Contexts

Modern web compositing requires handling scrollable areas and accumulated visual effects. The pipeline executes two critical assignments:

  • ViewportPaintable::assign_scroll_frames() creates ScrollFrame objects for scrollable paintables and links them into a coherent scroll state tree.
  • ViewportPaintable::assign_accumulated_visual_contexts() assigns identifiers for accumulated visual contexts used by effects such as backdrop-filter.

These steps ensure that scroll offsets and complex visual filters are correctly propagated through the rendering tree before any pixels are drawn.

Recording the Display List

The core of the compositor resides in ViewportPaintable::paint_all_phases(DisplayListRecordingContext&). This method iterates through the painting phases defined by PaintPhase (Background, Foreground, Overlay, and others), executing the following for each phase:

  • Invokes the appropriate paint_* helpers on each Paintable (e.g., paint_background, paint_text, paint_border).
  • When a paintable has opacity, blend-mode, filter, or mask, emits an ApplyEffects command storing the effect parameters.

All commands accumulate in a DisplayListRecorder (see Libraries/LibWeb/Painting/DisplayListRecorder.h), creating an intermediate representation of the frame that decouples painting logic from the final rasterization backend.

Blend Modes and Compositing Operators

CSS blend modes are translated to low-level graphics operators in Libraries/LibWeb/Painting/Blending.cpp. The function mix_blend_mode_to_compositing_and_blending_operator() maps CSS mix-blend-mode values to Gfx::CompositingAndBlendingOperator enums.

When a paintable's computed style contains mix-blend-mode or isolation, the compositor records the corresponding operator in the display list. During rasterization, these operators determine how pixel colors from different layers are mathematically combined.

Rasterization with Skia

The recorded display list moves to Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp for execution. The DisplayListPlayerSkia class walks the command list and issues Skia drawing calls.

If a command carries a non-normal compositing_and_blending_operator, the Skia paint object receives a custom blender via Gfx::to_skia_blender. This ensures that blend modes and compositing effects are executed with GPU acceleration where available, producing the final pixel buffer.

Presentation and the Compositor Loop

After rasterization completes, the resulting bitmap copies to the window's front buffer (or to an off-screen surface for further compositing). The continuous rendering cycle is driven by HTML::RenderingThread::compositor_loop() in Libraries/LibWeb/HTML/RenderingThread.cpp, which monitors dirty state and triggers the full pipeline whenever the page requires updates.

Triggering a Paint Programmatically

Developers interacting with Ladybird's internals can force a complete repaint through the C++ API. The following example demonstrates the exact sequence of calls used by the rendering thread:

// Example: force a full repaint of the current page.
auto& page = browser->active_page();                     // Page* from UI layer
auto& paintable = page->layout().viewport();             // ViewportPaintable&
auto display_list_context = Web::Painting::DisplayListRecordingContext::make(
    painter, palette, device_pixel_ratio, chrome_metrics);

// 1️⃣ Build stacking contexts (only needed if layout changed)
paintable.build_stacking_context_tree_if_needed();

// 2️⃣ Assign scroll frames & visual contexts (once per layout)
paintable.assign_scroll_frames();
paintable.assign_accumulated_visual_contexts();

// 3️⃣ Record all painting phases into the display list
paintable.paint_all_phases(display_list_context);

// 4️⃣ Play the list with Skia (actual rasterisation)
Web::Painting::DisplayListPlayerSkia player(display_list_context.recorder());
player.play();

Each function above is implemented in the files referenced throughout this pipeline description.

Key Source Files

The painting and compositing implementation spans these critical components:

Summary

  • Layout converts to Paintables: The ViewportPaintable root manages the tree of drawable objects derived from layout nodes.
  • Stacking contexts establish order: build_stacking_context_tree_if_needed() handles z-index and visual effect scoping.
  • Scroll and visual states are assigned: assign_scroll_frames() and assign_accumulated_visual_contexts() prepare complex effects.
  • Display list decouples painting from rasterization: paint_all_phases() records commands into a DisplayListRecorder for backend-agnostic rendering.
  • Skia executes the final composite: DisplayListPlayerSkia applies blend modes via Gfx::to_skia_blender and outputs the final buffer.
  • Compositor loop drives updates: RenderingThread::compositor_loop() continuously monitors for dirty state and triggers repaints.

Frequently Asked Questions

What is a Paintable in Ladybird?

A Paintable is the rendering representation of a layout node. While the layout tree handles geometry and box calculations, Paintable objects (such as PaintableBox, TextPaintable, and SVGPaintable) know how to generate drawing commands. The ViewportPaintable serves as the root container for the entire viewport's drawable content.

How does Ladybird handle CSS mix-blend-mode?

Ladybird translates CSS mix-blend-mode values to internal graphics operators in Blending.cpp using mix_blend_mode_to_compositing_and_blending_operator(). These operators are stored as ApplyEffects commands in the display list. During rasterization, DisplayListPlayerSkia applies them via Gfx::to_skia_blender, ensuring that layer blending occurs with GPU acceleration.

What is the role of the display list in Ladybird's compositor?

The display list is an intermediate command buffer that decouples the painting logic from the graphics backend. DisplayListRecorder captures all drawing operations, clips, and effects as serializable commands. This allows the engine to optimize, cache, or replay the sequence without re-executing CSS layout or paint logic, and enables backend-specific implementations like the Skia player.

How does the compositor loop know when to repaint?

The HTML::RenderingThread::compositor_loop() continuously monitors the page's dirty state. When layout changes, animations progress, or user interactions modify the visual state, the loop triggers the full painting pipeline—from stacking context construction through Skia rasterization—ensuring the window buffer stays synchronized with the document state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →