ladybird/Services/Compositor/CompositorState.cpp
Aliaksandr Kalenik 5c3e61efb6 Compositor: Rasterize nested contexts during replay
Nested compositor contexts used the same vsync scheduling path as
top-level contexts. That made iframe presents depend on a context's own
timer instead of the display refresh rate, and screenshots needed a
separate descendant flush to avoid stale child surfaces.

Keep top-level contexts on vsync, but make nested contexts dirty their
containing context instead of scheduling themselves. When a composited
child surface is resolved, synchronously rasterize the child if it has
a pending frame, using a separate display-list player so recursive
replay does not reuse the active player state. This lets screenshots and
normal presentation share the same child-surface update path.
2026-06-19 17:09:44 +02:00

578 lines
21 KiB
C++

/*
* Copyright (c) 2026, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Math.h>
#include <AK/StdLibExtras.h>
#include <Compositor/CompositorState.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
namespace Compositor {
static constexpr int gpu_completion_check_interval_ms = 1;
NonnullRefPtr<CompositorState> CompositorState::create(RefPtr<Gfx::SkiaBackendContext> skia_backend_context, bool async_scrolling_enabled)
{
return adopt_ref(*new CompositorState(move(skia_backend_context), async_scrolling_enabled));
}
CompositorState::CompositorState(RefPtr<Gfx::SkiaBackendContext> skia_backend_context, bool async_scrolling_enabled)
: m_skia_backend_context(move(skia_backend_context))
, m_display_list_player(make<Web::Painting::DisplayListPlayerSkia>(m_skia_backend_context))
, m_async_scrolling_enabled(async_scrolling_enabled)
{
}
CompositorState::~CompositorState()
{
if (!m_gpu_completion_timer)
return;
m_gpu_completion_timer->on_timeout = {};
m_gpu_completion_timer->stop();
}
void CompositorState::set_client(CompositorStateClient& client)
{
m_client = &client;
}
CompositorState::ContextOwnerCheckResult CompositorState::check_context_owner(Web::Compositor::CompositorContextId context_id, CompositorStateWebContentClient& client)
{
auto* context = context_if_present(context_id);
if (!context)
return ContextOwnerCheckResult::ContextUnavailable;
if (!context->is_owned_by(client))
return ContextOwnerCheckResult::ConflictingOwner;
return ContextOwnerCheckResult::OwnedByClient;
}
void CompositorState::destroy_contexts_for_web_content_client(CompositorStateWebContentClient& client)
{
Vector<Web::Compositor::CompositorContextId> context_ids;
for (auto& context : m_contexts) {
if (context.value->is_owned_by(client))
context_ids.append(context.key);
}
for (auto context_id : context_ids) {
destroy_context(context_id);
}
}
void CompositorState::create_context(Web::Compositor::CompositorContextId context_id, Optional<u64> page_id, CompositorStateWebContentClient& web_content_client)
{
VERIFY(!m_contexts.contains(context_id));
if (page_id.has_value())
VERIFY(context_id == Web::Compositor::compositor_context_id_for_page(*page_id));
auto& context = *m_contexts.ensure(context_id, [&] {
return make<ContextState>(page_id, web_content_client, m_canvas_surface_registry, m_async_scrolling_enabled);
});
resize_backing_stores_if_needed(context_id, context);
}
void CompositorState::destroy_context(Web::Compositor::CompositorContextId context_id)
{
auto* context = context_if_present(context_id);
VERIFY(context);
cancel_pending_async_presents_for_context(context_id);
clear_parent_context(*context);
for (auto& context_entry : m_contexts) {
if (context_entry.key == context_id)
continue;
auto& possible_child_context = *context_entry.value;
auto parent_context_id = possible_child_context.parent_context_id();
if (parent_context_id.has_value() && *parent_context_id == context_id)
possible_child_context.set_parent_context({});
}
m_contexts.remove(context_id);
}
void CompositorState::set_parent_context(Web::Compositor::CompositorContextId context_id, Optional<Web::Compositor::CompositorContextId> parent_context_id)
{
auto* context = context_if_present(context_id);
VERIFY(context);
if (parent_context_id.has_value()) {
VERIFY(!context->presents_to_client());
VERIFY(*parent_context_id != context_id);
VERIFY(context_if_present(*parent_context_id));
}
auto current_parent_context_id = context->parent_context_id();
if (current_parent_context_id.has_value() == parent_context_id.has_value()
&& (!current_parent_context_id.has_value() || *current_parent_context_id == *parent_context_id))
return;
clear_parent_context(*context);
context->set_parent_context(parent_context_id);
if (!parent_context_id.has_value())
return;
if (!context->latest_rendered_surface() && !context->needs_rasterization())
return;
auto* parent_context = context_if_present(*parent_context_id);
VERIFY(parent_context);
present_current_frame(*parent_context_id, *parent_context);
}
void CompositorState::stop_presenting_to_client(Web::Compositor::CompositorContextId context_id)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->stop_presenting_to_client();
}
void CompositorState::update_display_list(Web::Compositor::CompositorContextId context_id, NonnullRefPtr<Web::Painting::DisplayList> display_list, Web::Painting::AccumulatedVisualContextTree visual_context_tree, Web::Painting::DisplayListResourceTransaction&& resource_transaction, Web::Painting::ScrollStateSnapshot&& scroll_state_snapshot)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->apply_display_list_resource_transaction(move(resource_transaction));
context->install_display_list_update(move(display_list), move(visual_context_tree), move(scroll_state_snapshot));
}
void CompositorState::update_visual_context_tree(Web::Compositor::CompositorContextId context_id, Web::Painting::AccumulatedVisualContextTree visual_context_tree)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->update_visual_context_tree(move(visual_context_tree));
}
void CompositorState::update_scroll_state(Web::Compositor::CompositorContextId context_id, Web::Painting::ScrollStateSnapshot&& scroll_state_snapshot)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->update_scroll_state(move(scroll_state_snapshot));
}
void CompositorState::update_video_frame(Web::Compositor::CompositorContextId context_id, Web::Painting::VideoFrameResourceId frame_id, NonnullRefPtr<Media::VideoFrame const> frame)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->update_video_frame(frame_id, move(frame));
present_current_frame(context_id, *context);
}
void CompositorState::clear_video_frame(Web::Compositor::CompositorContextId context_id, Web::Painting::VideoFrameResourceId frame_id)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->clear_video_frame(frame_id);
present_current_frame(context_id, *context);
}
void CompositorState::invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId context_id, u64 generation)
{
auto* context = context_if_present(context_id);
VERIFY(context);
context->invalidate_wheel_event_listener_state(generation);
}
bool CompositorState::handle_mouse_event(Web::Compositor::CompositorContextId context_id, Web::MouseEvent const& event)
{
auto* context = context_if_present(context_id);
if (!context)
return false;
return apply_context_update_result(context_id, *context, context->handle_mouse_event(event));
}
bool CompositorState::dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId context_id, Web::MouseEvent const& event)
{
auto* context = context_if_present(context_id);
if (!context)
return false;
context->dispatch_mouse_event_to_web_content(event);
return true;
}
bool CompositorState::handle_pinch_event(Web::Compositor::CompositorContextId context_id, Web::PinchEvent const& event)
{
auto* context = context_if_present(context_id);
if (!context)
return false;
return apply_context_update_result(context_id, *context, context->handle_pinch_event(event));
}
Web::Compositor::AsyncScrollEnqueueResult CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking)
{
if (!m_async_scrolling_enabled)
return {};
auto* context = context_if_present(context_id);
VERIFY(context);
auto result = context->async_scroll_by(expected_document_id, position, delta, viewport_rect, operation_tracking);
if (result.frame_to_present.has_value())
schedule_present_frame(context_id, *context, *result.frame_to_present);
return result.enqueue_result;
}
bool CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta)
{
if (!m_async_scrolling_enabled)
return false;
auto* context = context_if_present(context_id);
if (!context)
return false;
return apply_context_update_result(context_id, *context, context->async_scroll_by(position, delta));
}
bool CompositorState::should_defer_main_thread_present_for_async_scroll(Web::Compositor::CompositorContextId context_id) const
{
auto* context = context_if_present(context_id);
VERIFY(context);
return context->should_defer_main_thread_present_for_async_scroll();
}
Web::Compositor::PendingAsyncScrollUpdates CompositorState::take_pending_async_scroll_updates(Web::Compositor::CompositorContextId context_id)
{
auto* context = context_if_present(context_id);
VERIFY(context);
return context->take_pending_async_scroll_updates();
}
void CompositorState::viewport_size_updated(Web::Compositor::CompositorContextId context_id, Gfx::IntSize viewport_size, Web::Compositor::WindowResizingInProgress window_resize_in_progress)
{
auto* context = context_if_present(context_id);
if (!context)
return;
context->viewport_size_updated(viewport_size, window_resize_in_progress);
resize_backing_stores_if_needed(context_id, *context);
if (context->should_shrink_backing_stores_after_resize())
schedule_backing_store_shrink(context_id, *context);
}
void CompositorState::set_display_metadata(Web::Compositor::CompositorContextId context_id, Optional<u64> display_id, double refresh_rate)
{
auto* context = context_if_present(context_id);
if (!context)
return;
VERIFY(refresh_rate == refresh_rate);
VERIFY(refresh_rate > 0);
VERIFY(refresh_rate < AK::Infinity<double>);
if (context->set_display_metadata(display_id, refresh_rate))
schedule_pending_present_frame(context_id, *context);
}
void CompositorState::present_frame(Web::Compositor::CompositorContextId context_id, Gfx::IntRect viewport_rect)
{
auto* context = context_if_present(context_id);
VERIFY(context);
schedule_present_frame(context_id, *context, viewport_rect);
}
void CompositorState::present_frame(Web::Compositor::CompositorContextId context_id, ContextState& context, Gfx::IntRect viewport_rect)
{
auto composited_context_resolver = resolver_for(context_id);
auto prepared_frame = context.prepare_frame(*m_display_list_player, viewport_rect, &composited_context_resolver);
if (!prepared_frame.has_value())
return;
m_pending_async_presents.append(context_id, viewport_rect, prepared_frame->bitmap_id);
auto* pending_present = &m_pending_async_presents.last();
auto& event_loop = Core::EventLoop::current();
auto self = NonnullRefPtr { *this };
m_display_list_player->flush_async(*prepared_frame->rendered_surface, [self = move(self), &event_loop, pending_present] {
event_loop.deferred_invoke([self = move(self), pending_present] {
self->did_finish_async_present(*pending_present);
});
});
context.did_submit_prepared_frame(viewport_rect);
schedule_gpu_completion_check();
}
void CompositorState::schedule_present_frame(Web::Compositor::CompositorContextId context_id, ContextState& context, Gfx::IntRect viewport_rect)
{
context.queue_present_frame(viewport_rect);
schedule_pending_present_frame(context_id, context);
}
void CompositorState::schedule_pending_present_frame(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
if (!context.presents_to_client()) {
schedule_containing_context_present(context);
return;
}
schedule_pending_present_frame_on_vsync(context_id, context);
}
void CompositorState::schedule_pending_present_frame_on_vsync(Web::Compositor::CompositorContextId, ContextState& context)
{
context.mark_pending_present_frame_scheduled();
vsync_scheduler_for_display(context.display_id()).schedule(context.display_refresh_rate());
}
void CompositorState::schedule_containing_context_present(ContextState& context)
{
auto parent_context_id = context.parent_context_id();
if (!parent_context_id.has_value())
return;
auto* parent_context = context_if_present(*parent_context_id);
VERIFY(parent_context);
present_current_frame(*parent_context_id, *parent_context);
}
void CompositorState::schedule_pending_present_frame_if_unblocked(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
if (!context.can_schedule_pending_present_frame_if_unblocked())
return;
schedule_pending_present_frame(context_id, context);
}
VSyncScheduler& CompositorState::vsync_scheduler_for_display(Optional<u64> display_id)
{
return *m_vsync_schedulers_by_display.ensure(display_id, [this, display_id] {
return create_vsync_scheduler(display_id, [this, display_id] {
present_pending_frames_on_vsync(display_id);
});
});
}
void CompositorState::present_pending_frames_on_vsync(Optional<u64> display_id)
{
for (auto& context_entry : m_contexts) {
auto context_id = context_entry.key;
auto& context = *context_entry.value;
if (!context.has_pending_present_frame_scheduled_on(display_id))
continue;
auto pending_present_frame = context.take_pending_present_frame_if_unblocked();
if (!pending_present_frame.has_value())
continue;
present_frame(context_id, context, *pending_present_frame);
}
}
bool CompositorState::request_screenshot(Web::Compositor::CompositorContextId context_id, Gfx::ShareableBitmap& target_bitmap)
{
auto* context = context_if_present(context_id);
VERIFY(context);
if (!context->can_paint_screenshot(target_bitmap))
return false;
auto composited_context_resolver = resolver_for(context_id);
context->paint_screenshot(*m_display_list_player, target_bitmap, &composited_context_resolver);
return true;
}
void CompositorState::presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId context_id, i32 bitmap_id)
{
auto* context = context_if_present(context_id);
if (!context)
return;
if (!context->acknowledge_presented_bitmap(bitmap_id))
return;
schedule_pending_present_frame_if_unblocked(context_id, *context);
}
void CompositorState::did_finish_async_present(PendingAsyncPresent& pending_present)
{
auto pending_present_iterator = m_pending_async_presents.begin();
for (; pending_present_iterator != m_pending_async_presents.end(); ++pending_present_iterator) {
if (&*pending_present_iterator == &pending_present)
break;
}
VERIFY(pending_present_iterator != m_pending_async_presents.end());
auto context_id = pending_present.context_id;
auto viewport_rect = pending_present.viewport_rect;
auto bitmap_id = pending_present.bitmap_id;
auto was_cancelled = pending_present.was_cancelled;
(void)m_pending_async_presents.remove(pending_present_iterator);
if (m_pending_async_presents.is_empty() && m_gpu_completion_timer)
m_gpu_completion_timer->stop();
if (was_cancelled)
return;
auto* context = context_if_present(context_id);
VERIFY(context);
context->did_finish_gpu_present(bitmap_id);
if (context->presents_to_client()) {
VERIFY(m_client);
m_client->did_present_frame(context_id, viewport_rect, bitmap_id);
}
if (auto parent_context_id = context->parent_context_id(); parent_context_id.has_value()) {
auto* parent_context = context_if_present(*parent_context_id);
VERIFY(parent_context);
present_current_frame(*parent_context_id, *parent_context);
}
schedule_pending_present_frame_if_unblocked(context_id, *context);
}
void CompositorState::cancel_pending_async_presents_for_context(Web::Compositor::CompositorContextId context_id)
{
for (auto& pending_present : m_pending_async_presents) {
if (pending_present.context_id == context_id)
pending_present.was_cancelled = true;
}
}
void CompositorState::schedule_gpu_completion_check()
{
if (!m_skia_backend_context)
return;
VERIFY(!m_pending_async_presents.is_empty());
if (!m_gpu_completion_timer) {
m_gpu_completion_timer = Core::Timer::create_repeating(gpu_completion_check_interval_ms, [this] {
check_gpu_completions();
});
}
if (!m_gpu_completion_timer->is_active())
m_gpu_completion_timer->start();
}
void CompositorState::check_gpu_completions()
{
if (m_pending_async_presents.is_empty()) {
if (m_gpu_completion_timer)
m_gpu_completion_timer->stop();
return;
}
if (m_skia_backend_context)
m_skia_backend_context->check_async_work_completion();
if (m_pending_async_presents.is_empty() && m_gpu_completion_timer)
m_gpu_completion_timer->stop();
}
ContextState* CompositorState::context_if_present(Web::Compositor::CompositorContextId context_id)
{
auto it = m_contexts.find(context_id);
if (it == m_contexts.end())
return nullptr;
return it->value.ptr();
}
ContextState const* CompositorState::context_if_present(Web::Compositor::CompositorContextId context_id) const
{
auto it = m_contexts.find(context_id);
if (it == m_contexts.end())
return nullptr;
return it->value.ptr();
}
void CompositorState::clear_parent_context(ContextState& context)
{
auto parent_context_id = context.parent_context_id();
if (!parent_context_id.has_value())
return;
context.set_parent_context({});
auto* parent_context = context_if_present(*parent_context_id);
if (!parent_context)
return;
present_current_frame(*parent_context_id, *parent_context);
}
CompositedContextResolver CompositorState::resolver_for(Web::Compositor::CompositorContextId parent_context_id)
{
return [this, parent_context_id](Web::Compositor::CompositorContextId child_context_id) {
return resolve_composited_context(parent_context_id, child_context_id);
};
}
RefPtr<Gfx::PaintingSurface> CompositorState::resolve_composited_context(Web::Compositor::CompositorContextId parent_context_id, Web::Compositor::CompositorContextId child_context_id)
{
auto* child_context = context_if_present(child_context_id);
if (!child_context)
return nullptr;
auto child_parent_context_id = child_context->parent_context_id();
if (!child_parent_context_id.has_value() || *child_parent_context_id != parent_context_id)
return nullptr;
if (child_context->needs_rasterization()) {
auto composited_context_resolver = resolver_for(child_context_id);
Web::Painting::DisplayListPlayerSkia display_list_player { m_skia_backend_context };
child_context->present_synchronously(display_list_player, &composited_context_resolver);
}
return child_context->latest_rendered_surface();
}
void CompositorState::resize_backing_stores_if_needed(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
if (auto publication = context.resize_backing_stores_if_needed(m_skia_backend_context); publication.has_value()) {
publish_backing_stores(context_id, context, publication.release_value());
present_current_frame(context_id, context);
}
}
void CompositorState::schedule_backing_store_shrink(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
context.schedule_backing_store_shrink([this, context_id] {
shrink_backing_stores_after_resize(context_id);
});
}
void CompositorState::shrink_backing_stores_after_resize(Web::Compositor::CompositorContextId context_id)
{
auto* context = context_if_present(context_id);
if (!context)
return;
context->finish_window_resize();
resize_backing_stores_if_needed(context_id, *context);
}
void CompositorState::present_current_frame(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
// A queued frame already captures the newest viewport rect and will pick up
// the updated resource when the outstanding bitmap is acknowledged.
auto frame_to_present = context.current_frame_rect_to_present();
if (frame_to_present.has_value())
schedule_present_frame(context_id, context, *frame_to_present);
}
bool CompositorState::apply_context_update_result(
Web::Compositor::CompositorContextId context_id,
ContextState& context,
ContextState::ContextUpdateResult const& result)
{
if (result.frame_to_present.has_value())
schedule_present_frame(context_id, context, *result.frame_to_present);
if (result.should_request_rendering_update)
context.request_rendering_update();
return result.accepted;
}
void CompositorState::publish_backing_stores(Web::Compositor::CompositorContextId context_id, ContextState& context, BackingStoreManager::Publication&& publication)
{
VERIFY(m_client);
VERIFY(context.presents_to_client());
m_client->did_allocate_backing_stores(context_id, publication.front_bitmap_id, move(publication.front_shared_image), publication.back_bitmap_id, move(publication.back_shared_image));
}
}