When image data becomes available, intrinsic dimensions can change, but
that does not require relayout for image elements whose computed width,
height, and min/max constraints are independent of intrinsic sizing. In
that case, invalidate paint and intrinsic-size caches instead of marking
layout dirty.
Keep the conservative relayout path for auto, percentage,
calc-percentage, and intrinsic sizing constraints, since decoded image
dimensions can affect used size in indefinite contexts. Query image
availability while painting so an existing paintable can switch from alt
text to image content without relayout.
Add text coverage that waits for image load events and checks auto,
percentage, calc-percentage, intrinsic keyword, min/max percentage, and
fixed-size cases without relying on timeouts.
Similar to GC::Root<T>, make GC::RootVector<T> constructible without
explicitly passing a Heap.
This is implemented by having RootVectorBase use GC::Heap::the() for
heap-free construction.
When loading an image, there are two success paths: Either the image is
loaded through fetch, or it is part of the list of available images
already and doesn't need to be downloaded anymore.
Only the fetch code path was invalidating style and layout, meaning that
an image loaded from the list of available images was not visible until
something else caused an invalidation.
To fix this, perform the same invalidation in both code paths.
This fixes that most of the images on https://bleepingcomputer.com/ were
not loading until hovered. They are using
[`bLazy.JS`](https://github.com/dinbror/blazy), which loads the files on
a disconnected `img` element and then swaps out `src` or `srcset` on the
actual `img` element once the load has completed.
The `requestAnimationFrame`-dance in the testcase is there to ensure
that the test fails reliably without this change applied.
Prune decoded image resources retained by active documents once they
grow beyond a recent working set. Route-heavy applications can load
many unique images through one Document, and the shared resource and
available-image caches would otherwise keep decoded image data alive
after the DOM stopped using those images.
Track cache touches and evict least recently used decoded images from
both caches. This keeps active documents from accumulating unbounded
decoded image resources while preserving a small hot cache for repeat
loads.
DecodedImageFrame only wraps a ref-counted Bitmap and color-space
metadata. The frame object itself does not provide shared mutable
state or lifetime ownership beyond those members, so ref-counting it
adds an unnecessary layer of indirection.
The Paintable tree and its supplemental painting data structures were
GC allocated because that was the easiest way to manage it and avoid
leaks introduced by ref cycles. This included the Paintable subclasses
themselves plus StackingContext, ChromeWidget, Scrollbar, ResizeHandle,
and scroll-frame state.
We are now trying to reduce GC allocation churn on layout and painting
updates, so keeping this short-lived rendering tree outside the JS heap
is a better fit. Move Paintable to RefCountedTreeNode, make painting
helpers ref-counted or weakly reference Paintables, and update the
layout and event-handler call sites to use RefPtr/WeakPtr ownership.
DecodedImageFrame now owns decoded bitmap pixels directly, so the
separate ImmutableBitmap wrapper no longer carries useful semantics.
Remove the class and pass decoded image frames or bitmaps at the
boundaries where pixels are actually required.
The Skia image cache now keys off DecodedImageFrame, matching the
display-list commands that paint decoded images. Video frames stay
owned by LibMedia, with the explicit YUV-to-bitmap conversion living
at HTMLVideoElement's decoded-frame entry point for canvas and WebGL
callers.
Decoded image data should not continue to traffic in ImmutableBitmap now
that the bitmap wrapper is being retired. Introduce DecodedImageFrame as
the paintable decoded-image unit and store a Bitmap plus ColorSpace in
it directly.
Thread the new frame type through decoded image data, display-list
image commands, filters, canvas drawImage, patterns, WebGL texture
upload, and CSS/SVG image consumers. ImmutableBitmap remains only at
the legacy boundaries that still need it, such as HTML video snapshots
and callers that explicitly ask for a bitmap snapshot.
This keeps color-space ownership with the decoded frame while making
the expensive or legacy ImmutableBitmap path explicit at the few call
sites that still need it.
Previously, presentational hints bypassed the regular cascade pipeline
and wrote directly into `CascadedProperties` under
`CascadeOrigin::Author`. That meant `var()` substitution and the
invalid-at-computed-value-time fallback had to be duplicated in a
separate per-element pass, which in practice missed the IACVT step and
could leave a `GuaranteedInvalidStyleValue` in the cascaded
properties. This caused a crash in downstream code that assumed the
value had been resolved.
This introduces an `AuthorPresentationalHint` cascade origin and feeds
them through the cascade as normal declarations. This means that
`var()` resolution now happens in only one place.
Extract the file-local is_supported_image_type() helper from
HTMLImageElement into a small standalone translation unit so other
parts of the engine can ask the same question. The next commit reuses
it for the image-set() type() filter.
The list is still hard-coded; deriving it from the registered image
decoders remains a FIXME.
HTMLImageElement's update-the-image-data step 16 queues its state
transition and load event dispatch via a 1 ms BatchingDispatcher, so
the current request does not become CompletelyAvailable synchronously
when the fetch finishes. decode()'s on_finish callback, however, was
queuing its resolve task directly on the event loop, bypassing the
batch. That race meant decode() could resolve while the image request
was still in Unavailable state, so any .then() handler inspecting
img.width / img.height (or anything derived from the bitmap) would see
zeros.
Google Maps hits this on its .9.png road shield icons: after awaiting
img.decode() it reads a.width / a.height and calls
ctx.getImageData(0, 0, 0, 0), which throws IndexSizeError and aborts
the tile rendering pipeline.
Route decode()'s on_finish through the same BatchingDispatcher so both
are processed in the same batch, with the decode resolution queued
after step 16's element task.
Previously, the LibWeb bindings generator would output multiple per
interface files like Prototype/Constructor/Namespace/GlobalMixin
depending on the contents of that IDL file.
This complicates the build system as it means that it does not know
what files will be generated without knowledge of the contents of that
IDL file.
Instead, for each IDL file only generate a single Bindings/<IDLFile>.h
and Bindings/<IDLFile>.cpp.
Our implementation of image decoding and its effect on
presenting/rendering the DOM is already effectively "async". The value
"auto" is implementation-defined, but should likely default to async
behavior as well. That leaves us with `decoding="sync"`, for which we
should still show a FIXME.
Replace per-frame heap-allocated RefCounted ScrollFrame objects with a
single contiguous Vector<ScrollFrame> inside ScrollState. All frames for
a viewport are now stored in one allocation, using type-safe
ScrollFrameIndex instead of RefPtr pointers.
This reduces allocation churn, improves cache locality, and moves
parent-chain traversal (cumulative offset, nearest scrolling ancestor)
into ScrollState — similar to how visual context nodes were recently
consolidated into AccumulatedVisualContextTree.
Introduce Document::update_layout_if_needed_for_node() which only calls
update_layout() when the node is connected. Use it at all call sites
that query layout metrics (offsets, client dimensions, image size, SVG
bounding box, etc.) so disconnected elements no longer trigger an
unnecessary layout.
Rename Document::set_needs_display() to set_needs_repaint() and make it
private. External callers must now go through Node/Paintable which
route the request to the document internally.
Fix one existing misuse in AnimationEffect that was calling
document-level set_needs_display() instead of routing through the
target element's paintable.
This is preparation for per-paintable display list command caching:
repaint requests must go through specific paintables so their cached
command lists can be invalidated.
When img.src is changed rapidly (e.g., YouTube Music sets a GIF
placeholder then swaps to a real URL via IntersectionObserver), the
failure callback from the stale first request could corrupt the newer
request by calling abort_the_image_request on the now-reassigned
m_current_request.
Fix this by using the existing m_update_the_image_data_count generation
counter to detect stale fetch callbacks.
This fixes thumbnail loading on YouTube Music.
Add unsafe_layout_node(), unsafe_paintable(), and unsafe_paintable_box()
accessors that skip layout-staleness verification. These are for use in
contexts where accessing layout/paintable data is legitimate despite
layout not being up to date: tree construction, style recalculation,
painting, animation interpolation, DOM mutation, and invalidation
propagation.
Also add wrapper APIs on Node to centralize common patterns:
- set_needs_display() wraps if (unsafe_paintable()) ...set_needs_display
- set_needs_paint_only_properties_update() wraps similar
- set_needs_layout_update() wraps if (unsafe_layout_node()) ...
And add Document::layout_is_up_to_date() which checks whether layout
tree update flags are all clear.
When an img element is changed from animated image to static image, the
animation briefly continues into the new image, even if the new image
has only a single frame (static image).
Could also impact when going from animated image to another animated
image, but the new image has less frames versus the previously animated
image.
In some cases a newly loaded static image would continue to be animated,
so that is also stopped.
fixes: #7879fixes: #7945
Add AnimatedDecodedImageData which implements DecodedImageData with
an 8-slot buffer pool instead of storing all frames in memory.
Frames are requested on demand from the ImageDecoder service as
the animation progresses.
For a 344-frame animated image at 1920x1080, this reduces
WebContent memory from ~1.3 GB to ~66 MB.
The streaming class owns frame progression and synchronizes
multiple callers (HTMLImageElement and ImageStyleValue) through
notify_frame_advanced() returning the authoritative frame index.
When a frame isn't in the pool, the last displayed frame is shown
as a fallback (brief freeze rather than blank).
Rename the old AnimatedBitmapDecodedImageData (which now only
handles static/single-frame images) to BitmapDecodedImageData.
HTMLImageElement's "update the image data" algorithm checks
is_fully_active() at the start, but its async continuations
(microtasks, element tasks, batching dispatcher callbacks) skip
this check. When an iframe is removed or navigated, these
callbacks fire on an inactive document, causing crashes.
Fix this with two changes:
1) Add is_fully_active() bail-out checks at all async callback
entry points in HTMLImageElement. Each bail-out also clears
the DocumentLoadEventDelayer to prevent blocking the parent
document's load event forever.
2) Create the DocumentObserver eagerly in initialize() (like
HTMLMediaElement) with a document_became_inactive callback
that clears the load event delayer and stops the animation
timer. Fire document_became_inactive from Document::destroy()
in addition to did_stop_being_active_document_in_navigable(),
since iframe removal takes a different path than navigation.
A guard flag prevents duplicate firing.
Prevents observably calling Trusted Types, which can run arbitrary JS,
cause crashes due to use of MUST and allow arbitrary JS to modify
internal elements.
Previously we would always use the window's viewport which was incorrect
if we were within an iframe.
This is likely applicable to all uses of
`Length::ResolutionContext::for_window`.
Currently, ImageProvider::current_image_bitmap takes a Gfx::IntSize
argument which determines the size of the returned bitmap. The default
value of this argument is 0x0 which causes the function to return
nullptr. This behavior is evidently unintuitive enough that it has lead
to incorrect usage in multiple places. For example, the 2D canvas
drawImage method will never actually draw anything because it calls
current_image_bitmap with no arguments. And the naturalWidth and
naturalHeight of an image will always return 0 (even after the image has
loaded) for the same reason.
To correct this and hopefully avoid similar issues in the future,
ImageProvider::current_image_bitmap will be renamed to
current_image_bitmap_sized, and the default value for the size argument
will be removed. For consistency, a similar change will be made to
SVGImageElement::default_image_bitmap.
The existing current_image_bitmap function will no longer take a size
argument. Instead it will always return a bitmap of the image's
intrinsic size. This seems to be what most existing callers had already
assumed was the function's behavior.
Documents created by DOMParser and fragment documents do not
have an origin set on the document by the spec. These documents
also happen to never become fully active.
By properly implementing the steps for the <img> element to only
update the image data for documents which are fully active, this
fixes a crash for img elements in these types of documents.
Unfortunately, this is not a full fix for the microtask queue case.
This is because it seems possible for node document for an <img>
element to be changed during the microtask queue for that document.
It is not clear to me how this can be fixed in a nice way.
Before this change, we were going through the chain of base classes for
each IDL interface object and having them set the prototype to their
prototype.
Instead of doing that, reorder things so that we set the right prototype
immediately in Foo::initialize(), and then don't bother in all the base
class overrides.
This knocks off a ~1% profile item on Speedometer 3.
12c6ac78e2 with fixed mistake when cache
slot is copied instead of being referenced:
```cpp
auto cache =
box.cached_intrinsic_sizes().min_content_height.ensure(width);
```
while it should've been:
```cpp
auto& cache =
box.cached_intrinsic_sizes().min_content_height.ensure(width);
```
This change moves intrinsic sizes cache from
LayoutState, which is local to current layout run,
to layout nodes, so it could be reused between
layout runs. This optimization is possible because
we can guarantee that these measurements will
remain unchanged unless the style of the element
or any of its descendants changes.
For now, invalidation is implemented simply by
resetting cache on whole ancestors chain once we
figured that element needs layout update.
The case when layout is invalidated by DOM's
structural changes is covered by layout tree
invalidation that drops intrinsic sizes cache
along with layout nodes.
I measured improvement on couple websites:
- Mail list on GMail 28ms -> 6ms
- GitHub large code page 47ms -> 36ms
- Discord chat history 15ms -> 8ms
(Time does not include `commit()`)