Commit Graph

48 Commits

Author SHA1 Message Date
Jelle Raaijmakers
90a211bf47 LibWeb: Use device-pixel coordinates in display list and AVC
Stop converting between CSS and device pixels as part of rendering - the
display list should be as simple as possible, so convert to DevicePixels
once when constructing the display list.
2026-02-26 07:43:00 +01:00
Aliaksandr Kalenik
abb392bd65 LibWeb: Remove redundant flush() in DisplayListPlayer::execute_impl()
The flush() call at the end of execute_impl() was accidentally left
behind in 2d2af9cd3b. That commit moved flushing into execute(), but
didn't remove the old call from execute_impl(). This caused every
nested display list to trigger a redundant GPU flush.

On an M4 MacBook, this improves Discord from ~65 FPS to 120 FPS.
2026-02-24 18:50:02 +01:00
Aliaksandr Kalenik
efbefb3b59 LibWeb: Skip display list commands under zero-area clips
Add a pre-computed `has_empty_effective_clip` flag on
AccumulatedVisualContext that propagates from parent to child. When a
clip rect or clip path has zero area, all descendant commands are
skipped at display list recording time in `DisplayList::append()`,
so they are never stored or executed.

This allows skipping ~10% of display list commands in the Discord app.
2026-02-24 16:41:20 +01:00
Aliaksandr Kalenik
37ec6a08a8 LibWeb: Rename is_clip_or_mask to is_clip
The mask case was removed when AddMask was replaced with saveLayer+DstIn
compositing, so this flag now only applies to clip commands.
2026-02-24 07:14:16 +01:00
Aliaksandr Kalenik
2d2af9cd3b LibWeb: Replace m_surfaces stack with m_surface in DisplayListPlayer
After removing the AddMask display list command, no code path ever
pushes more than one surface onto the stack.
2026-02-24 07:14:16 +01:00
Aliaksandr Kalenik
0ac72e40d4 LibWeb: Remove the unused AddMask display list command
All mask call sites now use saveLayer+DstIn compositing, so the AddMask
command, its SkSL runtime shaders, and CachedRuntimeEffects are no
longer needed.
2026-02-24 07:14:16 +01:00
Aliaksandr Kalenik
c7dc2ba0d3 LibWeb: Remove DrawPaintingSurface
No callers of draw_painting_surface remain after the previous commits
migrated canvas, video, and SVG to use ExternalContentSource or
ImmutableBitmap snapshots.
2026-02-20 18:41:33 +01:00
Aliaksandr Kalenik
291078dd87 LibWeb: Introduce ExternalContentSource
Two related problems exist in the current display list architecture:

1. DrawPaintingSurface thread safety: CanvasPaintable::paint() records
   the *same* PaintingSurface that the canvas rendering context draws
   to. The rendering thread later reads from it, but the main thread
   may be concurrently drawing — a data race.

2. Video frames force display list rebuilds: each new video frame
   triggers set_needs_display() → full display list rebuild.

Both stem from display list commands holding direct references to
content (surface/bitmap) rather than going through an indirection
layer.

ExternalContentSource is a thread-safe, atomically-refcounted
container that holds an ImmutableBitmap snapshot. The accompanying
DrawExternalContent display list command reads from it during replay,
so producers can swap in new content without rebuilding the list.

Subsequent commits migrate canvas, video, and SVG painting to
ExternalContentSource and then remove DrawPaintingSurface.
2026-02-20 18:41:33 +01:00
Jelle Raaijmakers
ca45464c87 LibWeb: Scale down perspective transformation by DPR in DisplayList
By doing so, we attenuate the perspective transform on higher resolution
devices such as Retina displays (2x).

This fixes the perspective transform on sites such as
https://poke-holo.simey.me/.
2026-02-19 21:31:21 +01:00
Aliaksandr Kalenik
fcd155b257 LibWeb: Remove ApplyTransform display list command
ApplyTransform is no longer recorded to the display list. Transforms are
now applied inline during display list execution when switching between
accumulated visual contexts.

Change apply_transform to accept parameters directly instead of the
ApplyTransform struct.
2026-01-23 18:56:24 +01:00
Aliaksandr Kalenik
cb8ecb3c11 LibGfx+LibWeb: Move SVG mask/clip composition from CPU to GPU
Previously, both mask and clip-path were rendered to separate mutable
Gfx::Bitmap objects which forced CPU rasterization. They were then
combined using a CPU pixel-by-pixel operation before being returned
as an ImmutableBitmap.

Instead of including mask in the final bitmap as already rasterized
images, we now use display lists which opens opportunity to utilize
GPU if available.

Bitmap::apply_mask() and ApplyMaskBitmap display list command are no
longer used and have been removed.
2026-01-23 16:23:06 +01:00
Aliaksandr Kalenik
cd0705334b LibWeb: Skip effect contexts for off-screen display list commands
When executing display list commands, check if commands with effect
contexts (opacity, filters, blend modes) are outside the viewport
before applying the effect. Since effects don't affect clip state,
would_be_fully_clipped_by_painter() returns the same result before
and after applying effects.

This avoids expensive saveLayer/restore cycles for off-screen commands
with effects like blur, which is particularly beneficial for pages with
many blurred decorative images (e.g., Discord's landing page has 70+
blurred star images).

The optimization only applies when switching to a new effect context,
not for consecutive commands with the same context, to preserve correct
blend mode compositing behavior.
2026-01-22 16:27:52 +01:00
Aliaksandr Kalenik
333395b625 LibWeb: Avoid unnecessary copies during display list execution 2026-01-21 19:10:26 +01:00
Aliaksandr Kalenik
96a39aeaa6 LibWeb: Move effects application into AccumulatedVisualContext
Effects (opacity, blend mode, filters) must be applied in the parent's
coordinate space, before the element's transform. Previously this was
handled by manually switching to the parent's visual context when
applying effects at paint time.

By adding EffectsData to AccumulatedVisualContext and positioning it
before TransformData in the chain, effects are now naturally applied in
the correct order during display list replay, eliminating the special
case in StackingContext::paint().

For SVG filters that can generate content from empty elements (feFlood,
feImage, feTurbulence), a transparent FillRect command is emitted to
trigger the filter through the same AVC pipeline.
2026-01-21 16:19:18 +01:00
Aliaksandr Kalenik
98afd82491 LibWeb: Integrate clip-path into AccumulatedVisualContext
Previously, clip-path was applied only during painting in
StackingContext::paint(), which meant hit testing did not respect
clip-path boundaries. Clicks outside the visible clipped region but
inside the element's bounding box would incorrectly register as hits.

By moving clip-path into AccumulatedVisualContext, it becomes part of
the same system that handles transforms, clips, and scroll offsets for
both painting and hit testing, ensuring consistent behavior.
2026-01-16 13:39:02 +01:00
Aliaksandr Kalenik
932c2999e4 LibWeb: Delete unused Push{Pop}StackingContext commands
Remove the PushStackingContext and PopStackingContext display list
commands that are no longer used after the AccumulatedVisualContext
integration.

Previously, PushStackingContext was responsible for:
- Applying CSS transforms via its StackingContextTransform field
- Managing opacity layers
- Handling clip paths
- Tracking blend modes

All of this functionality has been replaced by:
- Transform/perspective tracking via AccumulatedVisualContext nodes
- The ApplyEffects command for opacity, blend modes, and filters
- The AddClipPath command for clip paths
2026-01-15 19:50:53 +01:00
Aliaksandr Kalenik
a87b5c722d LibWeb: Add AccumulatedVisualContext debugging infrastructure 2026-01-15 19:50:53 +01:00
Aliaksandr Kalenik
009ddd4823 LibWeb: Integrate AccumulatedVisualContext with display list
Integrate AccumulatedVisualContext with display list recording and
playback. This is the main commit of the refactoring that delivers the
architectural improvements enabled by AccumulatedVisualContext.

Recording changes:

Each display list command now stores a single
RefPtr<AccumulatedVisualContext> instead of separate scroll_frame_id
and ClipFrame. The recorder simply captures the current accumulated
context when appending commands.

The before_paint()/after_paint() hooks that pushed/popped scroll frame
IDs are replaced by directly setting accumulated_visual_context on the
recorder before painting each element.

Playback changes:

The display list player now uses LCA (Lowest Common Ancestor) based
traversal to switch between visual contexts efficiently. When
transitioning from context A to context B:

1. Find the LCA of A and B in the context tree
2. Pop (restore) states back to the LCA depth
3. Push (save + apply) states from LCA down to B

This approach minimizes redundant save/restore operations. For example,
when rendering siblings that share a common scroll container, the
player keeps that scroll state applied and only switches the divergent
parts of their context chains.

Key deletions:

- Remove translate_by() from all 45 display list commands - commands
  are now immutable
- Remove transform/perspective fields from PushStackingContext -
  transforms are tracked via AccumulatedVisualContext
- Remove push_scroll_frame_id()/pop_scroll_frame_id() from
  DisplayListRecorder
- Remove before_paint()/after_paint() hooks from Paintable
- Merge ApplyOpacity, ApplyCompositeAndBlendingOperator, ApplyFilter
  into single ApplyEffects command

Stacking context painting changes:

The StackingContext::paint() method is significantly simplified.
Instead of building a PushStackingContextParams struct with transform
matrices and pushing/popping stacking contexts, it now:

1. Sets the accumulated visual context (which already contains
   transforms)
2. Applies effects (opacity, blend mode, filters) if needed
3. Applies clip path if needed
4. Paints the content
5. Restores state

The visual state management that was interleaved throughout the
painting code is now handled uniformly by the context tree.
2026-01-15 19:50:53 +01:00
Aliaksandr Kalenik
98660ad2b7 LibWeb: Introduce AddClipPath display list item
Introduce a new AddClipPath display list command that applies a
path-based clip region. This command is needed for the upcoming removal
of PushStackingContext, which currently handles clip paths as part of
its functionality. By extracting clip path handling into a dedicated
command, we can eliminate PushStackingContext entirely in favor of the
more granular AccumulatedVisualContext approach.
2026-01-15 19:50:53 +01:00
InvalidUsernameException
a08c175da2 LibWeb: Display clip rectangles when dumping display list 2025-12-01 17:46:44 +01:00
Jelle Raaijmakers
ba40da5e5f LibWeb: Rename DisplayList's apply_filters to apply_filter 2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
e4de6c0d05 LibWeb: Return early if scroll offset is zero
No need to calculate scroll offsets and translate update list commands
if the cumulative offset was zero to begin with.
2025-10-27 16:42:27 -07:00
Psychpsyo
2fcacd21f5 LibWeb: Compute bounds for paint-contained stacking contexts
This makes it so that the bounds for any paint-contained stacking
context are not derived from its children, but rather just set to
the rectangle that they will be clipped to anyways due to the paint
containment. Should make rendering faster on pages that use paint
containment.
2025-09-25 15:10:10 +02:00
Aliaksandr Kalenik
ba2926f8b3 LibWeb: Calculate and use bounds for "simple" stacking contexts
Teach the display list executor to derive a bounding rectangle for
stacking contexts whose inner commands can all report bounds, that is,
most contexts without nested stacking contexts.

This yields a large performance improvement on https://tc39.es/ecma262/
where the display list contains thousands of groups like:
```
PushStackingContext blending=Multiply
    DrawGlyphRun
PopStackingContext
```
Previously, `PushStackingContext` triggered an unbounded `saveLayer()`
even when the glyph run lies wholly outside the viewport. With this
change, we (1) cull stacking contexts that fall outside the viewport and
(2) provide bounds to `saveLayer()` when they are visible.

With this change rendering thread goes from 70% to 1% in profiles of
https://tc39.es/ecma262/. Also makes a huge performance difference on
Discord.
2025-09-24 22:11:24 +02:00
Aliaksandr Kalenik
a41d586117 LibWeb: Merge FillPathUsingPaintStyle and FillPathUsingColor
Use `Variant<PaintStyle, Gfx::Color>` in new `FillPath` instead of
duplicating two almost identical display list items.
2025-08-03 10:42:33 +02:00
Aliaksandr Kalenik
5c11a541d3 LibWeb: Merge StrokePathUsingPaintStyle and StrokePathUsingColor
Use `Variant<PaintStyle, Gfx::Color>` in new `StrokePath` instead of
duplicating two almost identical display list items.
2025-08-03 10:42:33 +02:00
Aliaksandr Kalenik
e41c85ec47 LibWeb: Replace DrawTriangleWave with StrokePathUsingColor
There's no need to have separate display list item for drawing triangle
wave when we could simply use StrokePathUsingColor. By switching to
StrokePathUsingColor we could also reduce painting because it supports
filtering out by bounding box.
2025-08-03 10:42:33 +02:00
Jelle Raaijmakers
f28b7064ee LibWeb: Add indentation to display list dumps
Output display list dumps with an indentation level to show balanced
commands. It makes it much easier to see what is happening between e.g.
PushStackingContext and PopStackingContext, or SaveLayer and Restore.
2025-08-01 14:20:51 +02:00
Aliaksandr Kalenik
b265618bfb LibWeb: Rename Command to DisplayListCommand
Gives name more consistent with other display list related classes.
2025-08-01 05:25:56 -04:00
Aliaksandr Kalenik
8569124b87 LibWeb: Fix scroll state refresh in cached display list for iframes
6507d23 introduced a bug when snapshot for iframe is saved in
`PaintNestedDisplayList` and, since display lists are immutable, it's
not possible to update before the next repaint.

This change fixes the issue by moving `ScrollStateSnapshot` for
nested display lists from `PaintNestedDisplayList` to
`HashMap<NonnullRefPtr<DisplayList>, ScrollStateSnapshot>` that is
placed into pending rendering task, making it possible to update
snapshots for all display lists before the next repaint.

This change doesn't have a test because it's really hard to make a ref
test that will specifically check scenario when scroll offset of an
iframe is advanced after display list is cached. We already have
`Tests/LibWeb/Ref/input/scroll-iframe.html` but unfortunately it did
not catch this bug.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/5486
2025-07-26 11:53:21 -04:00
Aliaksandr Kalenik
66e1d599f8 LibWeb: Use scroll state from snapshot while applying clip rectangles
Fixes race condition introduced in eed47acb when rendering thread
accesses ScrollFrame that could be mutated in the middle of
rasterization by the main thread, resulting in broken rendering.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/5553
2025-07-24 13:03:23 -04:00
Aliaksandr Kalenik
eed47acb1f LibWeb: Expand ClipFrame into clip rectangles during display list replay
Until now, every paint phase of every PaintableBox injected its own
clipping sequence into the display list:
```
before_paint: Save
              AddClipRect (1)
              ...clip rectangles for each containing block with clip...
              AddClipRect (N)

paint:        ...paint phase items...

after_paint:  Restore
```

Because we ran that sequence for every phase of every box, Skia had to
rebuild clip stack `paint_phases * paintable_boxes` times. Worse,
usually most paint phases contribute no visible drawing at all, yet we
still had to emit clipping items because `before_paint()` has no way to
know that in advance.

This change takes a different approach:
- Clip information is now attached as metadata `ClipFrame` to each
  DisplayList item.
- `DisplayListPlayer` groups consecutive commands that share a
  `ClipFrame`, applying the clip once at the start of the group and
  restoring it once at the end.

Going from 10 ms to 5 ms in rasterization on Discord might not sound
like much, but keep in mind that for 60fps we have 16 ms per frame and
there is a lot more work besides display list rasterization we do in
each frame.

* https://discord.com/channels/1247070541085671459/1247090064480014443
  - DisplayList items:  81844  -> 3671
  - rasterize time:     10 ms  -> 5 ms
  - record time:        5 ms   -> 3 ms

* https://github.com/LadybirdBrowser/ladybird
  - DisplayList items:  7902  -> 1176
  - rasterize time:     4 ms  -> 4 ms
  - record time:        3 ms  -> 2 ms
2025-07-14 15:48:28 +02:00
Aliaksandr Kalenik
8ae7417445 LibWeb: Add internals call to dump display list
It's useful to have tests that dump display list items, so we can more
easily see how changes to the display list recording process affect the
output. Even the small sample test added in this commit shows that we
currently record an unnecessary AddClipRect item for empty paint phases.

For now, the dump doesn't include every single property of an item, but
we can shape it to include more useful information as we iterate on it.
2025-07-13 19:15:05 +02:00
Lucien Fiorini
0fcb574041 LibGfx+LibWeb: Turn Gfx::Filter into a SkImageFilter wrapper 2025-06-01 23:22:10 +02:00
Timothy Flynn
66e422b4f1 LibWeb: Draw a scrollbar gutter when the scrollbar is enlarged 2025-04-22 11:29:06 -04:00
Aliaksandr Kalenik
6507d23e29 LibWeb: Save ScrollState snapshot in DisplayList to avoid race condition
With this change we save a copy of of scroll state at the time of
recording a display list, instead of actual ScrollState pointer that
could be modifed by the main thread while display list is beings
rasterized on the rendering thread, which leads to a frame painted with
inconsistent scroll state.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/4288
2025-04-12 02:55:18 +02:00
Aliaksandr Kalenik
cf143cf118 LibWeb: Don't call flush() when no surface is passed in execute_impl()
`DisplayListPlayer::execute_impl()` can receive null surface if it was
invoked to rasterize nested display lists (we use them for iframes). In
this case we should not call `flush()` by the end of execution and wait
until outer display list execution will do that.
2025-04-02 23:22:38 +02:00
Aliaksandr Kalenik
fd147e6be0 LibWeb: Protect SkiaBackendContext with a mutex
The Skia Ganesh backend we currently use doesn't support painting from
multiple threads, which could happen before this change when the main
thread used Skia to paint on the HTML canvas while the rendering thread
was working on display list rasterization.

Fixes https://github.com/LadybirdBrowser/ladybird/issues/4172
2025-04-01 23:39:05 +02:00
Aliaksandr Kalenik
24527b6ae3 LibWeb: Pass PaintingSurface into DisplayListPlayer::execute()
Deleteing set_surface() makes DisplayListPlayer API a bit more intuitive
because now caller doesn't have to think whether it's necessary to
restore previous surface after execution, instead DisplayListPlayer
takes care of it by maintaining a stack of surfaces.
2025-04-01 23:39:05 +02:00
Glenn Skrzypczak
6906f1722a LibWeb/Painting: Add back SaveLayer command
This reverts commit 552dd18696.
2025-04-01 13:38:00 +02:00
Aliaksandr Kalenik
1229328adc Revert "LibWeb/Painting: Add SaveLayer command"
This reverts commit 1898643ba4.
2025-03-28 16:48:03 +00:00
Glenn Skrzypczak
1898643ba4 LibWeb/Painting: Add SaveLayer command
This adds a command for saving the current layer of the canvas.
This is useful for painting content onto a blank background in
isolation and later compositing it onto the canvas.
2025-03-28 09:41:06 +00:00
Psychpsyo (Cameron)
c7926b2212 LibWeb: Change while to for loop 2025-03-25 15:50:45 +00:00
Glenn Skrzypczak
0fe30886f5 LibWeb/CSS: Implement mix-blend-mode
This adds support for the `mix-blend-mode` CSS property.
2025-02-05 11:26:58 +00:00
Jelle Raaijmakers
342cb7addf LibGfx+LibWeb: Reuse DisplayListPlayer and PaintingSurface when possible
Previously, we were reinstantiating the DisplayListPlayer and
PaintingSurface on every paint.
2025-01-31 13:28:09 +01:00
Saksham Mittal
8562b0e33b LibWeb: Migrate CSS filter application to new ApplyFilters command
This helps reuse this code in other areas, such as for filters for SVGs
2024-11-23 20:20:12 +01:00
Sam Atkins
a6822986bb LibWeb/Painting: Apply clip and mask operations that are off-screen
These operations should still apply even if they are off screen, because
they affect painting of things outside of their bounding rectangles.

This commit makes us always apply these, regardless of if they are in
the visible region. However, if they are outside that region, we
replace them with simple clip-rect commands, which have the same
effect (not painting anything) but are cheaper than computing a full
mask bitmap.
2024-11-13 16:10:15 +01:00
Timothy Flynn
93712b24bf Everywhere: Hoist the Libraries folder to the top-level 2024-11-10 12:50:45 +01:00