Display lists used to own the resource storage needed to replay their command bytes. That kept the compositor tied to in-process object ownership: sending a display list update also meant sharing the same resource container with the recording side. Move resource storage out of DisplayList and make display list updates carry a transaction of resources to add and remove. Navigable now tracks the resources referenced by the current display list, sends only the delta to the compositor, and trims its recording-side storage to the active set. The compositor applies those transactions to its own storage before replacing the cached display list. This still carries in-process resource objects, but it puts the ownership boundary in the right place. Command bytes and resource lifetime are now synchronized explicitly, which is the shape needed before the compositor can receive serializable resource updates across a process boundary.
275 lines
10 KiB
C++
275 lines
10 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/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;
|
|
}
|
|
|
|
RefPtr<Painting::DisplayList> SVGDecodedImageData::record_display_list(Gfx::IntSize size, Painting::DisplayListResourceStorage& resource_storage) const
|
|
{
|
|
m_document->navigable()->set_viewport_size(size.to_type<CSSPixels>());
|
|
m_document->update_layout(DOM::UpdateLayoutReason::SVGDecodedImageDataRender);
|
|
return m_document->record_display_list({}, resource_storage);
|
|
}
|
|
|
|
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)
|
|
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, resource_storage, {}, 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)
|
|
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();
|
|
}
|
|
|
|
}
|