ladybird/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp
Aliaksandr Kalenik b36e6c9b97 Compositor+LibWeb: Pass AVC trees separately from display lists
Display lists owned the accumulated visual context tree through a
ref-counted pointer. That tied visual-context state to display-list
lifetime and made compositor updates treat the two as one unit, even
though AVC trees need to become independently updateable compositor
state.

Make accumulated visual context trees plain versioned values, have each
display list store the compatible tree version, and pass the matching
tree alongside display-list updates and replay calls. Replay verifies
that the provided tree matches the display list before executing it.

This prepares the compositor for receiving AVC tree updates separately
from display-list updates: it now accepts the tree as a separate update
parameter, stores it next to the display list, and uses that stored tree
for replay and async-scroll hit testing. Nested display-list resources
carry their own tree snapshots for the same version check.
2026-05-27 18:29:42 +01:00

297 lines
12 KiB
C++

/*
* Copyright (c) 2023-2025, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Checked.h>
#include <AK/NumericLimits.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/PaintingSurface.h>
#include <LibJS/Runtime/ExternalMemory.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListPlayerSkia.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
#include <LibWeb/Painting/DisplayListResourceStorage.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/SVG/SVGDecodedImageData.h>
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/XML/XMLDocumentBuilder.h>
#include <LibXML/Parser/Parser.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGDecodedImageData);
GC_DEFINE_ALLOCATOR(SVGDecodedImageData::SVGPageClient);
ErrorOr<GC::Ref<SVGDecodedImageData>> SVGDecodedImageData::create(JS::Realm& realm, GC::Ref<Page> host_page, URL::URL const& url, ReadonlyBytes data)
{
auto page_client = SVGPageClient::create(Bindings::main_thread_vm(), host_page);
auto page = Page::create(Bindings::main_thread_vm(), *page_client);
page->set_is_scripting_enabled(false);
page_client->m_svg_page = page.ptr();
page->set_top_level_traversable(Web::HTML::TraversableNavigable::create_a_new_top_level_traversable(*page, nullptr, {}));
GC::Ref<HTML::Navigable> navigable = page->top_level_traversable();
auto response = Fetch::Infrastructure::Response::create(navigable->vm());
response->url_list().append(url);
auto origin = URL::Origin::create_opaque();
auto navigation_params = navigable->heap().allocate<HTML::NavigationParams>(OptionalNone {},
navigable,
nullptr,
response,
nullptr,
nullptr,
HTML::OpenerPolicyEnforcementResult { .url = url, .origin = origin, .opener_policy = HTML::OpenerPolicy {} },
nullptr,
origin,
navigable->heap().allocate<HTML::PolicyContainer>(realm.heap()),
HTML::SandboxingFlagSet {},
HTML::OpenerPolicy {},
OptionalNone {},
HTML::UserNavigationInvolvement::None);
// FIXME: Use Navigable::navigate() instead of manually replacing the navigable's document.
auto document = MUST(DOM::Document::create_and_initialize(DOM::Document::Type::XML, "image/svg+xml"_string, navigation_params));
navigable->set_ongoing_navigation({});
navigable->active_document()->destroy();
navigable->set_active_document(document);
auto& window = as<HTML::Window>(HTML::relevant_global_object(document));
document->browsing_context()->window_proxy()->set_window(window);
XML::Parser parser(data, { .resolve_named_html_entity = resolve_named_html_entity });
XMLDocumentBuilder builder { document, XMLScriptingSupport::Disabled };
auto result = parser.parse_with_listener(builder);
if (result.is_error())
dbgln("SVGDecodedImageData: Failed to parse SVG: {}", result.error());
// Mark the document as completely loaded so that <use> elements
// (which defer cloning until the document is complete) resolve
// forward references to elements parsed after them.
document->completely_finish_loading();
auto* svg_root = document->first_child_of_type<SVG::SVGSVGElement>();
if (!svg_root) {
dbgln("SVGDecodedImageData: Invalid SVG input (no SVGSVGElement found)");
return Error::from_string_literal("SVGDecodedImageData: Invalid SVG input");
}
return realm.create<SVGDecodedImageData>(page, page_client, document, *svg_root);
}
SVGDecodedImageData::SVGDecodedImageData(GC::Ref<Page> page, GC::Ref<SVGPageClient> page_client, GC::Ref<DOM::Document> document, GC::Ref<SVG::SVGSVGElement> root_element)
: m_page(page)
, m_page_client(page_client)
, m_document(document)
, m_root_element(root_element)
{
}
SVGDecodedImageData::~SVGDecodedImageData() = default;
void SVGDecodedImageData::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_page);
visitor.visit(m_document);
visitor.visit(m_page_client);
visitor.visit(m_root_element);
}
static size_t surface_external_memory_size(Gfx::PaintingSurface const& surface)
{
auto surface_size = surface.size();
if (surface_size.is_empty())
return 0;
Checked<size_t> pixel_size = static_cast<size_t>(surface_size.width());
pixel_size *= static_cast<size_t>(surface_size.height());
pixel_size *= sizeof(u32);
if (pixel_size.has_overflow())
return NumericLimits<size_t>::max();
return pixel_size.value();
}
size_t SVGDecodedImageData::external_memory_size() const
{
size_t size = Base::external_memory_size();
size = JS::saturating_add_external_memory_size(size, JS::hash_map_external_memory_size(m_cached_rendered_frames));
for (auto const& cached_frame : m_cached_rendered_frames)
size = JS::saturating_add_external_memory_size(size, cached_frame.value.bitmap().data_size());
size = JS::saturating_add_external_memory_size(size, JS::hash_map_external_memory_size(m_cached_rendered_surfaces));
for (auto const& cached_surface : m_cached_rendered_surfaces)
size = JS::saturating_add_external_memory_size(size, surface_external_memory_size(*cached_surface.value));
return size;
}
Optional<Painting::DisplayListResource> SVGDecodedImageData::record_display_list(Gfx::IntSize size, Painting::DisplayListResourceStorage& resource_storage) const
{
if (auto it = m_cached_display_lists.find(size); it != m_cached_display_lists.end()) {
resource_storage.append_referenced_resources_from(it->value.resource_storage, it->value.display_list->command_bytes());
return Painting::DisplayListResource { *it->value.display_list, it->value.visual_context_tree };
}
// FIXME: Evict least used entries.
if (m_cached_display_lists.size() > 10)
m_cached_display_lists.remove(m_cached_display_lists.begin());
Painting::DisplayListResourceStorage local_storage;
m_document->navigable()->set_viewport_size(size.to_type<CSSPixels>());
m_document->update_layout(DOM::UpdateLayoutReason::SVGDecodedImageDataRender);
auto display_list = m_document->record_display_list({}, local_storage);
if (!display_list)
return {};
resource_storage.append_referenced_resources_from(local_storage, display_list->command_bytes());
auto document_paintable = m_document->paintable();
VERIFY(document_paintable);
auto visual_context_tree = document_paintable->visual_context_tree();
auto display_list_resource = Painting::DisplayListResource { *display_list, visual_context_tree };
m_cached_display_lists.set(size, CachedDisplayList { NonnullRefPtr<Painting::DisplayList> { *display_list }, move(visual_context_tree), move(local_storage) });
return display_list_resource;
}
RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::render_to_surface(Gfx::IntSize size) const
{
VERIFY(m_document->navigable());
if (size.is_empty())
return {};
if (auto it = m_cached_rendered_surfaces.find(size); it != m_cached_rendered_surfaces.end())
return it->value;
// Prevent the cache from growing too big.
// FIXME: Evict least used entries.
if (m_cached_rendered_surfaces.size() > 10)
m_cached_rendered_surfaces.remove(m_cached_rendered_surfaces.begin());
auto surface = Gfx::PaintingSurface::create_with_size(size, Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied);
Painting::DisplayListResourceStorage resource_storage;
auto display_list = record_display_list(size, resource_storage);
if (!display_list.has_value())
return nullptr;
switch (m_page_client->display_list_player_type()) {
case DisplayListPlayerType::SkiaGPUIfAvailable:
case DisplayListPlayerType::SkiaCPU: {
Painting::DisplayListPlayerSkia display_list_player;
display_list_player.execute(*display_list->display_list, display_list->visual_context_tree, resource_storage, {}, surface);
display_list_player.flush(*surface);
break;
}
default:
VERIFY_NOT_REACHED();
}
m_cached_rendered_surfaces.set(size, *surface);
return surface;
}
Optional<Gfx::DecodedImageFrame> SVGDecodedImageData::frame(size_t, Gfx::IntSize size) const
{
if (size.is_empty())
return {};
if (auto it = m_cached_rendered_frames.find(size); it != m_cached_rendered_frames.end())
return it->value;
// Prevent the cache from growing too big.
// FIXME: Evict least used entries.
if (m_cached_rendered_frames.size() > 10)
m_cached_rendered_frames.remove(m_cached_rendered_frames.begin());
auto decoded_frame = Gfx::DecodedImageFrame { *render_to_surface(size)->snapshot_bitmap() };
m_cached_rendered_frames.set(size, decoded_frame);
return decoded_frame;
}
Optional<CSSPixels> SVGDecodedImageData::intrinsic_width() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
m_document->update_style();
auto const root_element_style = m_root_element->computed_properties();
VERIFY(root_element_style);
auto const& width_value = root_element_style->size_value(CSS::PropertyID::Width);
if (width_value.is_length() && width_value.length().is_absolute())
return width_value.length().absolute_length_to_px();
return {};
}
Optional<CSSPixels> SVGDecodedImageData::intrinsic_height() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
m_document->update_style();
auto const root_element_style = m_root_element->computed_properties();
VERIFY(root_element_style);
auto const& height_value = root_element_style->size_value(CSS::PropertyID::Height);
if (height_value.is_length() && height_value.length().is_absolute())
return height_value.length().absolute_length_to_px();
return {};
}
Optional<CSSPixelFraction> SVGDecodedImageData::intrinsic_aspect_ratio() const
{
// https://www.w3.org/TR/SVG2/coords.html#SizingSVGInCSS
auto width = intrinsic_width();
auto height = intrinsic_height();
if (width.has_value() && height.has_value() && *width > 0 && *height > 0)
return *width / *height;
if (auto const& viewbox = m_root_element->view_box(); viewbox.has_value()) {
auto viewbox_width = CSSPixels::nearest_value_for(viewbox->width);
if (viewbox_width == 0)
return {};
auto viewbox_height = CSSPixels::nearest_value_for(viewbox->height);
if (viewbox_height == 0)
return {};
return viewbox_width / viewbox_height;
}
return {};
}
void SVGDecodedImageData::SVGPageClient::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_host_page);
visitor.visit(m_svg_page);
}
Optional<Gfx::IntRect> SVGDecodedImageData::frame_rect(size_t) const
{
return {};
}
RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::surface(size_t, Gfx::IntSize size) const
{
return render_to_surface(size);
}
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode) const
{
auto display_list = record_display_list(dst_rect.size(), context.display_list_recorder().resource_storage());
if (!display_list.has_value())
return;
context.display_list_recorder().save();
context.display_list_recorder().add_clip_rect(clip_rect);
context.display_list_recorder().paint_nested_display_list(*display_list, dst_rect);
context.display_list_recorder().restore();
}
}