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.
All mask call sites now use saveLayer+DstIn compositing, so the AddMask
command, its SkSL runtime shaders, and CachedRuntimeEffects are no
longer needed.
This applies the same pattern used for background-clip: text (commit
f2e6f70fbb).
Results in visible performance improvement in Discord app where
previously, according to profiles, we spent lots of time allocating
surfaces for masks.
No callers of draw_painting_surface remain after the previous commits
migrated canvas, video, and SVG to use ExternalContentSource or
ImmutableBitmap snapshots.
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.
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.
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.
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.
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.
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
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.
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.
Clip frames for overflow were applied based on whether the box in
question had a non-identity matrix transformation associated with it.
That however is not correct, since specifying a no-op transform like
`scale(1)` still needs to apply clip overflow rectangles. So instead we
need to check whether the element associated with the box in question
has any CSS transforms.
This appears to have been a regression from
9bbc1cd618 and effectively reverts that
commit, but keeps its effect by unifying on the check for CSS transforms
instead.
This fixes some background boxes being rendered for the invisible items
of the carousels on https://computerbase.de/.
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.
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.
Before this change we would emit PushStackingContext/PopStackingContext
display list items regardless of whether the stacking context had any
transform/opacity/clip effects.
Display list size on https://x.com/ladybirdbrowser is reduced from ~2700
to ~800 items.
We can use BorderRadiiData::as_corners() to avoid converting the corners
one by one. Instead of passing all four corners one by one, use a
reference to CornerRadii.
No functional changes.
`aa_translation` is something we inherited from times when
AntiAliasingPainter was a thing. This change replaces it by applying
offset directly to path.
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.
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
Skia allows you to pass a bounding rect to its saveLayer() function as
an optimization when you know that you won't paint outside those bounds.
Unfortunately, we were passing a too-small rectangle that didn't take
into account transformed descendants, etc.
For now, simply pass null instead of a bounding rect. This way, Skia
figures it out internally. It may allocate larger temporary bitmaps than
needed this way, but at least we get more correct results. I've left
re-enabling the optimization as a FIXME in the code.
This fixes unwanted clipping in various parts of the Discord UI.
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
Unbalanced save/restore within display list items recorded for a
paintable means that some state only relevant for the paintable leaks to
subsequent paintables, which is never expected behavior.
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
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.
Instead of trying to manually determine which parts of a bitmap fall
within the box of the `<img>` element, just draw the whole bitmap and
let Skia clip the draw-area to the correct rectangle.
This fixes a bug where the entire bitmap was squashed into the rectangle
of the image box instead of being clipped.
With this change, image rendering is now correct enough to import some
of the WPT tests for object-fit and object-position. To get some good
coverage I have imported all tests for the `<img>` tag. I also wanted to
import a subset of the tests for the `<object>` tag, since those are
passing as well now. Unfortunately, they are flaky for unknown reasons.
This is the second attempt at this bugfix. The prior one was e055927ead
and broke image rendering whenever the page was scrolled. It has
subsequently been reverted in 16b14273d1. Hopefully this time it is not
horribly broken.
Instead of trying to manually determine which parts of a bitmap fall
within the box of the `<img>` element, just draw the whole bitmap and
let Skia clip the draw-area to the correct rectangle.
This fixes a bug where the entire bitmap was squashed into the rectangle
of the image box instead of being clipped.
With this change, image rendering is now correct enough to import some
of the WPT tests for object-fit and object-position. To get some good
coverage I have imported all tests for the `<img>` tag. I also wanted to
import a subset of the tests for the `<object>` tag, since those are
passing as well now. Unfortunately, they are flaky for unknown reasons.