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.
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.
Move the inline dom_node() method to Viewport.cpp so the header no
longer needs the full Document definition. Add explicit includes to
files that relied on the transitive dependency.
Instead of computing and caching the transform matrix and transform
origin on PaintableBox during resolve_paint_properties(), compute them
inline during assign_accumulated_visual_contexts() where they are
actually consumed. This makes PaintableBox smaller by not wasting space
for properties that are only consumed during AccumulatedVisualContext
tree construction.
Apply the same fix from the previous commit to StackingContext hit test.
Hidden stacking context roots should still allow their visible children
to be hit.
When an element creates a stacking context (e.g. via position: relative
with z-index), its text fragments were not being hit tested. This was
because PaintableBox::hit_test() returns early when it has a stacking
context, and StackingContext::hit_test() only iterated child paintables,
not the stacking context root's own fragments.
Fix this by extracting fragment hit testing into a new method
hit_test_fragments() on PaintableWithLines, and calling it from
StackingContext::hit_test() when the stacking context root is a
PaintableWithLines.
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.
This moves filter resolution from display list recording time to
resolve_paint_properties(), caching the resolved values in CSS pixels
on PaintableBox. Device pixel conversion is now deferred until paint
time via to_gfx_filter().
This follows the existing pattern used for other paint-only properties
like box shadows and border radii.
Previously, outlines for elements without their own stacking context
were painted during the `FocusAndOverlay` phase, which runs after all
child stacking contexts. This caused outlines to incorrectly appear
on top of elements with higher z-index.
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.
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.
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
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.
...in clip and scroll frames calculation algorithm.
Fix a regression from 719a50c where display-list recording disagreed
with the clipping logic about whether a stacking context is transformed.
`has_css_transform()` returns true whenever the computed transform is
not `none`, which differs from an identity-matrix check. These yield
different results for cases like `translate(0, 0)`.
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.
If a node that establishes a StackingContext has `pointer-events: none`,
hit testing should first proceed with hit testing the SC's children
before deciding to bail. We were checking for `pointer-events` too
early, causing large parts of certain websites to be noninteractive.
Fixes#6017.
The transform of each paintable was being applied multiple times due to
the recursive nature of the hit testing methods. Previously it used
combined_css_transform to transform the position, and then it would pass
that position to children, which would then apply combined_css_transform
again, and so on.
PaintableBoxes are also not hit tested anymore when having a stacking
context. A similar check is done in PaintableWithLines, but it was
missing from PaintableBox. Without this check some elements can get
returned multiple times from a hit test.
StackingContexts with zero opacity will now also get hit tested, as it
should have been before.
PaintContext dates back to a time when display lists didn't exist and it
truly represented "paint context". Renaming it to better align with its
current role.
Having a setter for `device_pixels_per_css_pixel` was confusing because
display lists are immutable, so it doesn't make sense to override this
value after the display list has been created.
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.
83b6bc4 went too far by forbidding SVGSVGElement from establishing a
stacking context. This element type does follow the behavior of CSS
boxes, unlike inner SVG elements like `<rect>`, `<circle>`, etc., which
are not supposed to be aware of concepts like stacking contexts,
overflow clipping, scroll offsets, etc.
This change allows us to delete overrides of `before_paint()` and
`after_paint()` in SVGPaintable and SVGSVGPaintable, because display
list recording code has been rearranged to take care of clipping and
scrolling before recursing into SVGSVGPaintable descendants.
`Screenshot/images/css-transform-box-ref.png` expectation is updated and
fixes a bug where a rectangle at the very bottom of the page was not
clipped correctly.
`Screenshot/images/svg-filters-lb-website-ref.png` has a more subtle
difference, but if you look closely, you’ll see it matches other
browsers more closely now.
Unbalanced save and restore means that effects only relevant to a
stacking context leak outside, which is never expected behavior. Having
a `VERIFY()` for that makes it much easier to catch such issues.
These were only used in SVGSVGPaintable to apply scroll frame id, which
is already handled by `before_paint()` and `after_paint()` hooks in
PaintableBox.
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.
In `StackingContext::paint_descendants()`, we don't need to obtain the
computed values nor the Z-index of a child unless certain other
conditions are true. Let these conditions short-circuit before actually
reaching into the computed values, which shows up in profiles.
An early return was occurring between the emission of
PushStackingContext and PopStackingContext, resulting in a
PushStackingContext without a corresponding PopStackingContext in the
display list, which caused broken painting.
Fixes black screen on Discord login page.
When recording the display list for a stacking context, the following
operations (relevant to this bug) happened:
* push a stacking context
* as part of that push a None-value to the scroll frame id stack
* apply filters
* apply masking
* paint recursively
This meant that mask-images were always recorded without scroll frame
id, causing them to be painted without any scroll offset. As a result
mask-images would break as soon as the website using them was scrolled.
Instead, push to the scroll frame id stack later to solve the problem:
* push a stacking context
* apply filters
* apply masking
* push a None-value to the scroll frame id stack
* paint recursively
This is a improved version of a73cd88f0c
The old commit was reverted in 552dd18696
The new version only paints an element into a new layer if background
blend modes other than normal are used. The rasterization performance
of most websites should therefore not suffer.
Co-Authored-By: Alexander Kalenik <kalenik.aliaksandr@gmail.com>