LibWeb: Move backing store management into compositor thread

BackingStoreManager was owned by Navigable and allocated on the GC heap,
which left backing-store sizing decisions on the main thread even though
the compositor thread was already responsible for allocating the actual
surfaces. That split ownership makes it harder to isolate the compositor
behind a process boundary.

Move the manager into LibWeb's compositor code and let CompositorThread
thread data own it. The main thread now reports viewport size changes
through a compositor command, and the compositor uses that message to
decide when to resize and publish backing stores. The delayed shrink
timer remains with the main-thread CompositorThread facade so it can use
the Core event loop, but it now only sends another viewport-size update.

This removes the GC edge from Navigable, drops stale WebContent includes
and keeps the existing resize padding and delayed shrink behavior.
This commit is contained in:
Aliaksandr Kalenik 2026-05-16 15:32:14 +02:00 committed by Alexander Kalenik
parent fabe966949
commit fd823becd7
11 changed files with 155 additions and 157 deletions

View file

@ -32,6 +32,7 @@ set(SOURCES
Clipboard/SystemClipboard.cpp
Compositor/AsyncScrollTree.cpp
Compositor/AsyncScrollingState.cpp
Compositor/BackingStoreManager.cpp
Compositor/CompositorThread.cpp
Compression/CompressionStream.cpp
Compression/DecompressionStream.cpp
@ -849,7 +850,6 @@ set(SOURCES
Page/Page.cpp
Painting/AccumulatedVisualContext.cpp
Painting/BackgroundPainting.cpp
Painting/BackingStoreManager.cpp
Painting/Blending.cpp
Painting/BorderPainting.cpp
Painting/BorderRadiiData.cpp

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Compositor/BackingStoreManager.h>
#include <LibWeb/Compositor/CompositorThread.h>
namespace Web::Compositor {
Optional<BackingStoreManager::Allocation> BackingStoreManager::resize_backing_stores_if_needed(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress window_resize_in_progress)
{
if (viewport_size.is_empty())
return {};
auto minimum_needed_size = viewport_size;
bool force_reallocate = false;
if (window_resize_in_progress == WindowResizingInProgress::Yes && is_top_level_traversable) {
// Pad the minimum needed size so that we don't have to keep reallocating backing stores while the window is being resized.
minimum_needed_size = { viewport_size.width() + 256, viewport_size.height() + 256 };
} else {
// If we're not in the middle of a resize, we can shrink the backing store size to match the viewport size.
minimum_needed_size = viewport_size;
force_reallocate = m_allocated_size != minimum_needed_size;
}
if (force_reallocate || m_allocated_size.is_empty() || !m_allocated_size.contains(minimum_needed_size)) {
m_allocated_size = minimum_needed_size;
return Allocation {
.size = minimum_needed_size,
.front_bitmap_id = m_next_bitmap_id++,
.back_bitmap_id = m_next_bitmap_id++,
};
}
return {};
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Noncopyable.h>
#include <AK/Optional.h>
#include <AK/Types.h>
#include <LibGfx/Size.h>
#include <LibWeb/Export.h>
namespace Web::Compositor {
enum class WindowResizingInProgress : u8;
class WEB_API BackingStoreManager {
AK_MAKE_NONCOPYABLE(BackingStoreManager);
AK_MAKE_NONMOVABLE(BackingStoreManager);
public:
struct Allocation {
Gfx::IntSize size;
i32 front_bitmap_id { -1 };
i32 back_bitmap_id { -1 };
};
BackingStoreManager() = default;
Optional<Allocation> resize_backing_stores_if_needed(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress);
private:
int m_next_bitmap_id { 0 };
// Used to track if backing stores need reallocation
Gfx::IntSize m_allocated_size;
};
}

View file

@ -5,6 +5,7 @@
*/
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SharedImage.h>
@ -14,6 +15,7 @@
#include <LibThreading/Thread.h>
#include <LibWeb/Compositor/AsyncScrollTree.h>
#include <LibWeb/Compositor/AsyncScrollingState.h>
#include <LibWeb/Compositor/BackingStoreManager.h>
#include <LibWeb/Compositor/CompositorThread.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/Page/InputEvent.h>
@ -86,10 +88,10 @@ struct UpdateScrollStateCommand {
Painting::ScrollStateSnapshot scroll_state_snapshot;
};
struct UpdateBackingStoresCommand {
Gfx::IntSize size;
i32 front_bitmap_id;
i32 back_bitmap_id;
struct ViewportSizeUpdatedCommand {
Gfx::IntSize viewport_size;
bool is_top_level_traversable { false };
WindowResizingInProgress window_resize_in_progress { WindowResizingInProgress::No };
};
struct ScreenshotCommand {
@ -97,7 +99,8 @@ struct ScreenshotCommand {
Function<void()> callback;
};
using CompositorCommand = Variant<UpdateDisplayListCommand, AsyncScrollByCommand, ViewportScrollbarDragCommand, UpdateScrollStateCommand, UpdateBackingStoresCommand, ScreenshotCommand>;
using CompositorCommand = Variant<UpdateDisplayListCommand, AsyncScrollByCommand, ViewportScrollbarDragCommand,
UpdateScrollStateCommand, ViewportSizeUpdatedCommand, ScreenshotCommand>;
static SkRect to_skia_rect(Gfx::IntRect const& rect)
{
@ -802,14 +805,19 @@ public:
}
}
},
[this](UpdateBackingStoresCommand& cmd) {
[this](ViewportSizeUpdatedCommand& cmd) {
auto allocation = m_backing_store_manager.resize_backing_stores_if_needed(
cmd.viewport_size, cmd.is_top_level_traversable, cmd.window_resize_in_progress);
if (!allocation.has_value())
return;
if (m_has_async_scrolling_state) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor received backing stores front={} back={} size={}x{}",
cmd.front_bitmap_id, cmd.back_bitmap_id, cmd.size.width(), cmd.size.height());
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor resizing backing stores front={} back={} size={}x{}",
allocation->front_bitmap_id, allocation->back_bitmap_id, allocation->size.width(), allocation->size.height());
}
allocate_backing_stores(cmd);
m_backing_stores.front_bitmap_id = cmd.front_bitmap_id;
m_backing_stores.back_bitmap_id = cmd.back_bitmap_id;
allocate_backing_stores(*allocation);
m_backing_stores.front_bitmap_id = allocation->front_bitmap_id;
m_backing_stores.back_bitmap_id = allocation->back_bitmap_id;
},
[this](ScreenshotCommand& cmd) {
if (!m_cached_display_list)
@ -1158,40 +1166,42 @@ private:
m_skia_player = make<Painting::DisplayListPlayerSkia>(m_skia_backend_context);
}
void publish_backing_store_pair(UpdateBackingStoresCommand& cmd, Gfx::SharedImage front_shared_image, Gfx::SharedImage back_shared_image)
void publish_backing_store_pair(
BackingStoreManager::Allocation const& allocation, Gfx::SharedImage front_shared_image, Gfx::SharedImage back_shared_image)
{
if (!m_presents_to_client)
return;
VERIFY(CompositorThread::present_backing_stores_to_client(m_page_id, cmd.front_bitmap_id, move(front_shared_image), cmd.back_bitmap_id, move(back_shared_image)));
VERIFY(CompositorThread::present_backing_stores_to_client(
m_page_id, allocation.front_bitmap_id, move(front_shared_image), allocation.back_bitmap_id, move(back_shared_image)));
}
void allocate_backing_stores(UpdateBackingStoresCommand& cmd)
void allocate_backing_stores(BackingStoreManager::Allocation const& allocation)
{
#ifdef USE_VULKAN_DMABUF_IMAGES
if (m_skia_backend_context && m_presents_to_client) {
auto backing_stores = create_linear_dmabuf_backing_stores(cmd.size, *m_skia_backend_context);
auto backing_stores = create_linear_dmabuf_backing_stores(allocation.size, *m_skia_backend_context);
if (!backing_stores.is_error()) {
auto backing_store_pair = backing_stores.release_value();
m_backing_stores.front_store = move(backing_store_pair.front);
m_backing_stores.back_store = move(backing_store_pair.back);
publish_backing_store_pair(cmd, move(backing_store_pair.front_shared_image), move(backing_store_pair.back_shared_image));
publish_backing_store_pair(allocation, move(backing_store_pair.front_shared_image), move(backing_store_pair.back_shared_image));
return;
}
}
#endif
auto front_buffer = Gfx::SharedImageBuffer::create(cmd.size);
auto back_buffer = Gfx::SharedImageBuffer::create(cmd.size);
auto front_buffer = Gfx::SharedImageBuffer::create(allocation.size);
auto back_buffer = Gfx::SharedImageBuffer::create(allocation.size);
auto front_shared_image = front_buffer.export_shared_image();
auto back_shared_image = back_buffer.export_shared_image();
auto backing_store_pair = create_shareable_bitmap_backing_stores(cmd.size, front_buffer, back_buffer, m_skia_backend_context);
auto backing_store_pair = create_shareable_bitmap_backing_stores(allocation.size, front_buffer, back_buffer, m_skia_backend_context);
m_backing_stores.front_store = move(backing_store_pair.front);
m_backing_stores.back_store = move(backing_store_pair.back);
publish_backing_store_pair(cmd, move(front_shared_image), move(back_shared_image));
publish_backing_store_pair(allocation, move(front_shared_image), move(back_shared_image));
if (m_has_async_scrolling_state) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Allocated bitmap backing stores front={} back={} size={}x{}",
cmd.front_bitmap_id, cmd.back_bitmap_id, cmd.size.width(), cmd.size.height());
allocation.front_bitmap_id, allocation.back_bitmap_id, allocation.size.width(), allocation.size.height());
}
}
@ -1228,6 +1238,7 @@ private:
float m_viewport_scrollbar_thumb_grab_position { 0 };
mutable Sync::Mutex m_async_scroll_tree_mutex;
AsyncScrollTree m_async_scroll_tree;
BackingStoreManager m_backing_store_manager;
BackingStoreState m_backing_stores;
CompositorThread::PresentationMode m_presentation_mode { CompositorThread::PresentToUI {} };
@ -1305,12 +1316,20 @@ static FramePresentationState& frame_presentation_state()
CompositorThread::CompositorThread(u64 page_id, PagePresentationRegistration page_presentation_registration)
: m_thread_data(adopt_ref(*new ThreadData(page_id, Core::EventLoop::current_weak(), page_presentation_registration)))
{
m_backing_store_shrink_timer = Core::Timer::create_single_shot(3000, [this] {
enqueue_viewport_size_updated(m_last_viewport_size, m_last_viewport_size_is_top_level_traversable, WindowResizingInProgress::No);
});
if (page_presentation_registration == PagePresentationRegistration::Yes)
register_page_compositor(page_id, m_thread_data);
}
CompositorThread::~CompositorThread()
{
m_backing_store_shrink_timer->on_timeout = {};
m_backing_store_shrink_timer->stop();
m_backing_store_shrink_timer.clear();
unregister_page_compositor(m_thread_data->page_id(), *m_thread_data);
m_thread_data->exit();
}
@ -1573,9 +1592,21 @@ void CompositorThread::update_scroll_state(Painting::ScrollStateSnapshot&& scrol
m_thread_data->enqueue_command(UpdateScrollStateCommand { move(scroll_state_snapshot) });
}
void CompositorThread::update_backing_stores(Gfx::IntSize size, i32 front_id, i32 back_id)
void CompositorThread::viewport_size_updated(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress window_resize_in_progress)
{
m_thread_data->enqueue_command(UpdateBackingStoresCommand { size, front_id, back_id });
m_last_viewport_size = viewport_size;
m_last_viewport_size_is_top_level_traversable = is_top_level_traversable;
if (window_resize_in_progress == WindowResizingInProgress::Yes)
m_backing_store_shrink_timer->restart();
enqueue_viewport_size_updated(viewport_size, is_top_level_traversable, window_resize_in_progress);
}
void CompositorThread::enqueue_viewport_size_updated(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress window_resize_in_progress)
{
m_thread_data->enqueue_command(
ViewportSizeUpdatedCommand { viewport_size, is_top_level_traversable, window_resize_in_progress });
}
u64 CompositorThread::present_frame(Gfx::IntRect viewport_rect)

View file

@ -9,12 +9,14 @@
#include <AK/Noncopyable.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/Types.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibCore/Forward.h>
#include <LibGfx/Point.h>
#include <LibGfx/Rect.h>
#include <LibGfx/SharedImage.h>
#include <LibGfx/Size.h>
#include <LibSync/ConditionVariable.h>
#include <LibThreading/Forward.h>
#include <LibWeb/Compositor/AsyncScrollingState.h>
@ -25,6 +27,11 @@
namespace Web::Compositor {
enum class WindowResizingInProgress : u8 {
No,
Yes,
};
class WEB_API CompositorThread {
AK_MAKE_NONCOPYABLE(CompositorThread);
AK_MAKE_NONMOVABLE(CompositorThread);
@ -79,14 +86,19 @@ public:
bool should_defer_async_scroll_offset_adoption() const;
bool should_defer_main_thread_present_for_async_scroll() const;
PendingAsyncScrollUpdates take_pending_async_scroll_updates();
void update_backing_stores(Gfx::IntSize, i32 front_id, i32 back_id);
void viewport_size_updated(Gfx::IntSize, bool is_top_level_traversable, WindowResizingInProgress);
u64 present_frame(Gfx::IntRect);
void wait_for_frame(u64 frame_id);
void request_screenshot(NonnullRefPtr<Gfx::PaintingSurface>, Function<void()>&& callback);
private:
void enqueue_viewport_size_updated(Gfx::IntSize, bool is_top_level_traversable, WindowResizingInProgress);
NonnullRefPtr<ThreadData> m_thread_data;
RefPtr<Threading::Thread> m_thread;
RefPtr<Core::Timer> m_backing_store_shrink_timer;
Gfx::IntSize m_last_viewport_size;
bool m_last_viewport_size_is_top_level_traversable { false };
static void register_page_compositor(u64 page_id, NonnullRefPtr<ThreadData>);
static void unregister_page_compositor(u64 page_id, ThreadData&);

View file

@ -281,7 +281,6 @@ Navigable::Navigable(
: m_page(page)
, m_event_handler({}, *this)
, m_is_svg_page(is_svg_page)
, m_backing_store_manager(heap().allocate<Painting::BackingStoreManager>(*this))
, m_rendering_thread(
is_svg_page ? 0 : page->client().id(),
is_svg_page ? Compositor::CompositorThread::PagePresentationRegistration::No : page_presentation_registration)
@ -324,7 +323,6 @@ void Navigable::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_parent);
visitor.visit(m_active_document);
visitor.visit(m_container);
visitor.visit(m_backing_store_manager);
m_event_handler.visit_edges(visitor);
for (auto& navigation_params : m_pending_navigations) {
@ -2849,8 +2847,10 @@ void Navigable::set_viewport_size(CSSPixelSize size, InvalidateDisplayList inval
m_viewport_size = size;
if (!m_is_svg_page) {
m_backing_store_manager->restart_resize_timer();
m_backing_store_manager->resize_backing_stores_if_needed(Web::Painting::BackingStoreManager::WindowResizingInProgress::Yes);
m_rendering_thread.viewport_size_updated(
page().css_to_device_rect(viewport_rect()).size().to_type<int>(),
is_top_level_traversable(),
Compositor::WindowResizingInProgress::Yes);
m_pending_set_browser_zoom_request = false;
}

View file

@ -31,7 +31,6 @@
#include <LibWeb/HTML/WindowType.h>
#include <LibWeb/InvalidateDisplayList.h>
#include <LibWeb/Page/EventHandler.h>
#include <LibWeb/Painting/BackingStoreManager.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/XHR/FormDataEntry.h>
@ -200,8 +199,6 @@ public:
void wait_for_async_scroll_operation(Compositor::AsyncScrollOperationID, GC::Ref<WebIDL::Promise>);
void clamp_viewport_scroll_offset();
Painting::BackingStoreManager& backing_store_manager() { return *m_backing_store_manager; }
// https://html.spec.whatwg.org/multipage/webappapis.html#rendering-opportunity
[[nodiscard]] bool has_a_rendering_opportunity() const;
@ -333,7 +330,6 @@ private:
bool m_pending_set_browser_zoom_request { false };
bool m_should_show_line_box_borders { false };
Optional<PaintConfig> m_rendering_thread_display_list_paint_config;
GC::Ref<Painting::BackingStoreManager> m_backing_store_manager;
Compositor::CompositorThread m_rendering_thread;
RefPtr<Painting::ExternalContentSource> m_external_content_source;

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/Timer.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Painting/BackingStoreManager.h>
namespace Web::Painting {
GC_DEFINE_ALLOCATOR(BackingStoreManager);
BackingStoreManager::BackingStoreManager(HTML::Navigable& navigable)
: m_navigable(navigable)
{
m_backing_store_shrink_timer = Core::Timer::create_single_shot(3000, [this] {
resize_backing_stores_if_needed(WindowResizingInProgress::No);
});
}
void BackingStoreManager::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_navigable);
}
void BackingStoreManager::finalize()
{
Base::finalize();
m_backing_store_shrink_timer->on_timeout = {};
m_backing_store_shrink_timer->stop();
m_backing_store_shrink_timer.clear();
}
void BackingStoreManager::restart_resize_timer()
{
m_backing_store_shrink_timer->restart();
}
void BackingStoreManager::reallocate_backing_stores(Gfx::IntSize size)
{
m_front_bitmap_id = m_next_bitmap_id++;
m_back_bitmap_id = m_next_bitmap_id++;
m_allocated_size = size;
m_navigable->rendering_thread().update_backing_stores(size, m_front_bitmap_id, m_back_bitmap_id);
}
void BackingStoreManager::resize_backing_stores_if_needed(WindowResizingInProgress window_resize_in_progress)
{
if (m_navigable->is_svg_page())
return;
auto viewport_size = m_navigable->page().css_to_device_rect(m_navigable->viewport_rect()).size();
if (viewport_size.is_empty())
return;
Web::DevicePixelSize minimum_needed_size;
bool force_reallocate = false;
if (window_resize_in_progress == WindowResizingInProgress::Yes && m_navigable->is_top_level_traversable()) {
// Pad the minimum needed size so that we don't have to keep reallocating backing stores while the window is being resized.
minimum_needed_size = { viewport_size.width() + 256, viewport_size.height() + 256 };
} else {
// If we're not in the middle of a resize, we can shrink the backing store size to match the viewport size.
minimum_needed_size = viewport_size;
force_reallocate = m_allocated_size != minimum_needed_size.to_type<int>();
}
if (force_reallocate || m_allocated_size.is_empty() || !m_allocated_size.contains(minimum_needed_size.to_type<int>())) {
reallocate_backing_stores(minimum_needed_size.to_type<int>());
}
}
}

View file

@ -1,46 +0,0 @@
/*
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Export.h>
namespace Web::Painting {
class WEB_API BackingStoreManager : public JS::Cell {
GC_CELL(BackingStoreManager, JS::Cell);
GC_DECLARE_ALLOCATOR(BackingStoreManager);
public:
static constexpr bool OVERRIDES_FINALIZE = true;
enum class WindowResizingInProgress {
No,
Yes
};
void resize_backing_stores_if_needed(WindowResizingInProgress window_resize_in_progress);
void reallocate_backing_stores(Gfx::IntSize);
void restart_resize_timer();
virtual void finalize() override;
virtual void visit_edges(Cell::Visitor& visitor) override;
BackingStoreManager(HTML::Navigable&);
private:
GC::Ref<HTML::Navigable> m_navigable;
i32 m_front_bitmap_id { -1 };
i32 m_back_bitmap_id { -1 };
int m_next_bitmap_id { 0 };
// Used to track if backing stores need reallocation
Gfx::IntSize m_allocated_size;
RefPtr<Core::Timer> m_backing_store_shrink_timer;
};
}

View file

@ -13,7 +13,6 @@
#include <LibWeb/HTML/AudioPlayState.h>
#include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/BackingStoreManager.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWebView/Forward.h>

View file

@ -29,7 +29,6 @@
#include <LibWeb/Loader/ContentFilter.h>
#include <LibWeb/Loader/GeneratedPagesLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Painting/BackingStoreManager.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Platform/FontPlugin.h>