ladybird/Libraries/LibWeb/Compositor/CompositorThread.cpp
Aliaksandr Kalenik 81b5b343d9 LibWeb: Share WebContent compositor thread across pages
Move compositor-thread ownership to WebContent's PageHost so every Page
object in one WebContent process registers its navigables on the same
compositor thread. This covers auxiliary pages created by window.open(),
while worker and SVG helper pages continue to skip compositor thread
creation.

Keep page presentation keyed by page id. Each presenting context records
the page id it presents for, and static compositor entry points route
ready-to-paint, async scrolling, and viewport scrollbar events to that
page's presenting context on the shared thread.
2026-05-18 20:11:31 +02:00

1695 lines
82 KiB
C++

/*
* Copyright (c) 2025-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SharedImage.h>
#include <LibGfx/SkiaBackendContext.h>
#include <LibGfx/SkiaUtils.h>
#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>
#include <LibWeb/Painting/DisplayListPlayerSkia.h>
#include <AK/Debug.h>
#include <AK/HashMap.h>
#include <AK/NeverDestroyed.h>
#include <AK/Queue.h>
#include <AK/Time.h>
#include <core/SkCanvas.h>
#include <core/SkColor.h>
#include <core/SkPaint.h>
#include <core/SkRRect.h>
#include <gpu/ganesh/GrDirectContext.h>
#include <LibCore/Platform/ScopedAutoreleasePool.h>
namespace Web::Compositor {
static constexpr auto skia_deferred_cleanup_interval = AK::Duration::from_seconds(1);
static constexpr auto skia_aggressive_cleanup_interval = AK::Duration::from_seconds(5);
static constexpr auto skia_deferred_cleanup_resource_age = std::chrono::seconds(5);
static constexpr auto skia_resource_cache_high_watermark = 384 * MiB;
static constexpr auto skia_resource_cache_critical_watermark = 512 * MiB;
struct UpdateDisplayListCommand {
NonnullRefPtr<Painting::DisplayList> display_list;
Painting::DisplayListResourceTransaction resource_transaction;
Painting::ScrollStateSnapshot scroll_state_snapshot;
};
struct AsyncScrollByCommand {
Gfx::FloatPoint position;
Gfx::FloatPoint delta_in_device_pixels;
Gfx::IntRect viewport_rect;
AsyncScrollNodeID scroll_target;
Optional<AsyncScrollOperationID> operation_id;
};
struct ViewportScrollbarDragCommand {
size_t scrollbar_index { 0 };
float primary_position { 0 };
float thumb_grab_position { 0 };
};
struct UpdateScrollStateCommand {
Painting::ScrollStateSnapshot scroll_state_snapshot;
};
struct UpdateCompositorSurfaceCommand {
Painting::CompositorSurfaceId surface_id;
Gfx::SharedImage shared_image;
};
struct ClearCompositorSurfaceCommand {
Painting::CompositorSurfaceId surface_id;
};
struct ViewportSizeUpdatedCommand {
Gfx::IntSize viewport_size;
bool is_top_level_traversable { false };
WindowResizingInProgress window_resize_in_progress { WindowResizingInProgress::No };
};
struct ScreenshotCommand {
NonnullRefPtr<Gfx::PaintingSurface> target_surface;
Function<void()> callback;
};
using CompositorCommand = Variant<UpdateDisplayListCommand, AsyncScrollByCommand, ViewportScrollbarDragCommand,
UpdateScrollStateCommand, UpdateCompositorSurfaceCommand, ClearCompositorSurfaceCommand, ViewportSizeUpdatedCommand,
ScreenshotCommand>;
struct CompositorCommandEnvelope {
CompositorContextId context_id;
CompositorCommand command;
};
static SkRect to_skia_rect(Gfx::IntRect const& rect)
{
return SkRect::MakeXYWH(rect.x(), rect.y(), rect.width(), rect.height());
}
static Gfx::Orientation orientation_for_scrollbar(ViewportScrollbar const& scrollbar)
{
return scrollbar.vertical ? Gfx::Orientation::Vertical : Gfx::Orientation::Horizontal;
}
struct ViewportScrollbarIdentity {
AsyncScrollNodeID scroll_node_id;
bool vertical { false };
};
static ViewportScrollbarIdentity viewport_scrollbar_identity(ViewportScrollbar const& scrollbar)
{
return { scrollbar.scroll_node_id, scrollbar.vertical };
}
static Optional<ViewportScrollbarIdentity> viewport_scrollbar_identity_at(ReadonlySpan<ViewportScrollbar> scrollbars, Optional<size_t> scrollbar_index)
{
if (!scrollbar_index.has_value() || *scrollbar_index >= scrollbars.size())
return {};
return viewport_scrollbar_identity(scrollbars[*scrollbar_index]);
}
static Optional<size_t> find_viewport_scrollbar_index(ReadonlySpan<ViewportScrollbar> scrollbars, ViewportScrollbarIdentity identity)
{
for (size_t i = 0; i < scrollbars.size(); ++i) {
if (scrollbars[i].scroll_node_id == identity.scroll_node_id && scrollbars[i].vertical == identity.vertical)
return i;
}
return {};
}
static void set_or_append_pending_scroll_offset(Vector<AsyncScrollOffset>& pending_scroll_offsets, AsyncScrollOffset const& scroll_offset)
{
for (auto& existing : pending_scroll_offsets) {
if (existing.stable_node_id == scroll_offset.stable_node_id) {
existing.compositor_scroll_offset = scroll_offset.compositor_scroll_offset;
existing.unadopted_scroll_delta.translate_by(scroll_offset.unadopted_scroll_delta);
return;
}
}
pending_scroll_offsets.append(scroll_offset);
}
static Gfx::IntRect scrollbar_gutter_rect(ViewportScrollbar const& scrollbar, bool expanded)
{
return expanded ? scrollbar.expanded_gutter_rect : scrollbar.gutter_rect;
}
static double scrollbar_scroll_size(ViewportScrollbar const& scrollbar, bool expanded)
{
return expanded ? scrollbar.expanded_scroll_size : scrollbar.scroll_size;
}
static Gfx::IntRect translated_thumb_rect(ViewportScrollbar const& scrollbar, Painting::ScrollStateSnapshot const& scroll_state_snapshot, bool expanded)
{
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
auto scroll_size = scrollbar_scroll_size(scrollbar, expanded);
auto device_offset = scroll_state_snapshot.device_offset_for_index(scrollbar.scroll_frame_index);
if (scrollbar.vertical)
thumb_rect.translate_by(0, static_cast<int>(-device_offset.y() * scroll_size));
else
thumb_rect.translate_by(static_cast<int>(-device_offset.x() * scroll_size), 0);
return thumb_rect;
}
static Gfx::IntRect translated_thumb_rect(ViewportScrollbar const& scrollbar, Gfx::FloatPoint scroll_offset, bool expanded)
{
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
thumb_rect.translate_primary_offset_for_orientation(orientation, static_cast<int>(scroll_offset.primary_offset_for_orientation(orientation) * scrollbar_scroll_size(scrollbar, expanded)));
return thumb_rect;
}
static Gfx::IntRect scrollbar_hit_rect(ViewportScrollbar const& scrollbar, Gfx::FloatPoint scroll_offset)
{
static constexpr int scrollbar_hit_slop = 4;
auto rect = translated_thumb_rect(scrollbar, scroll_offset, false).united(translated_thumb_rect(scrollbar, scroll_offset, true));
auto expanded_gutter_rect = scrollbar_gutter_rect(scrollbar, true);
if (!expanded_gutter_rect.is_empty())
rect.unite(expanded_gutter_rect);
rect.inflate(scrollbar_hit_slop, scrollbar_hit_slop);
return rect;
}
static void paint_viewport_scrollbar(Gfx::PaintingSurface& surface, ViewportScrollbar const& scrollbar, Painting::ScrollStateSnapshot const& scroll_state_snapshot, bool expanded)
{
auto thumb_rect = translated_thumb_rect(scrollbar, scroll_state_snapshot, expanded);
auto& canvas = surface.canvas();
SkPaint gutter_fill_paint;
gutter_fill_paint.setColor(to_skia_color(scrollbar.track_color));
canvas.drawRect(to_skia_rect(scrollbar_gutter_rect(scrollbar, expanded)), gutter_fill_paint);
auto skia_thumb_rect = to_skia_rect(thumb_rect);
auto radius = skia_thumb_rect.width() / 2;
auto thumb_rrect = SkRRect::MakeRectXY(skia_thumb_rect, radius, radius);
SkPaint thumb_fill_paint;
thumb_fill_paint.setColor(to_skia_color(scrollbar.thumb_color));
canvas.drawRRect(thumb_rrect, thumb_fill_paint);
SkPaint stroke_paint;
stroke_paint.setStroke(true);
stroke_paint.setStrokeWidth(1);
stroke_paint.setAntiAlias(true);
stroke_paint.setColor(to_skia_color(scrollbar.thumb_color.lightened()));
canvas.drawRRect(thumb_rrect, stroke_paint);
}
static void paint_viewport_scrollbars(Gfx::PaintingSurface& surface, ReadonlySpan<ViewportScrollbar> scrollbars, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Optional<size_t> hovered_scrollbar_index, Optional<size_t> captured_scrollbar_index)
{
for (size_t i = 0; i < scrollbars.size(); ++i)
paint_viewport_scrollbar(surface, scrollbars[i], scroll_state_snapshot, hovered_scrollbar_index == i || captured_scrollbar_index == i);
}
static void flush_surface(Gfx::PaintingSurface& surface)
{
if (auto context = surface.skia_backend_context()) {
context->flush_and_submit(&surface.sk_surface());
auto* skia_context = context->sk_context();
static thread_local Optional<MonotonicTime> s_last_deferred_cleanup;
static thread_local Optional<MonotonicTime> s_last_aggressive_cleanup;
auto const now = MonotonicTime::now();
if (!s_last_deferred_cleanup.has_value() || now - *s_last_deferred_cleanup >= skia_deferred_cleanup_interval) {
s_last_deferred_cleanup = now;
skia_context->performDeferredCleanup(skia_deferred_cleanup_resource_age);
size_t resource_bytes = 0;
skia_context->getResourceCacheUsage(nullptr, &resource_bytes);
if (resource_bytes >= skia_resource_cache_high_watermark && (!s_last_aggressive_cleanup.has_value() || now - *s_last_aggressive_cleanup >= skia_aggressive_cleanup_interval)) {
s_last_aggressive_cleanup = now;
skia_context->performDeferredCleanup(std::chrono::milliseconds(0));
skia_context->getResourceCacheUsage(nullptr, &resource_bytes);
if (resource_bytes >= skia_resource_cache_critical_watermark)
skia_context->purgeUnlockedResources(GrPurgeResourceOptions::kScratchResourcesOnly);
}
}
}
surface.flush();
}
static constexpr u64 page_presenting_context_id_tag = 1ull << 63;
static CompositorContextId compositor_context_id_for_page(u64 page_id)
{
VERIFY((page_id & page_presenting_context_id_tag) == 0);
return CompositorContextId { page_presenting_context_id_tag | page_id };
}
static bool is_page_presenting_context_id(CompositorContextId context_id)
{
return (context_id.value() & page_presenting_context_id_tag) != 0;
}
class CompositorThread::ThreadData final : public AtomicRefCounted<ThreadData> {
public:
explicit ThreadData(NonnullRefPtr<Core::WeakEventLoopReference>&& main_thread_event_loop)
: m_main_thread_event_loop(move(main_thread_event_loop))
{
}
~ThreadData() = default;
struct ContextState final : public AtomicRefCounted<ContextState> {
ContextState(Optional<u64> page_id, CompositorThread::PagePresentationRegistration page_presentation_registration)
: presents_to_client(page_presentation_registration == CompositorThread::PagePresentationRegistration::Yes)
, page_id(page_id)
{
VERIFY(!presents_to_client || page_id.has_value());
}
bool presents_to_client { false };
Optional<u64> page_id;
Painting::DisplayListResourceStorage display_list_resource_storage;
RefPtr<Painting::DisplayList> cached_display_list;
Painting::ScrollStateSnapshot cached_scroll_state_snapshot;
Vector<ViewportScrollbar> viewport_scrollbars;
Optional<size_t> hovered_viewport_scrollbar_index;
Optional<size_t> captured_viewport_scrollbar_index;
float viewport_scrollbar_thumb_grab_position { 0 };
mutable Sync::Mutex async_scroll_tree_mutex;
AsyncScrollTree async_scroll_tree;
BackingStoreManager backing_store_manager;
CompositorThread::PresentationMode presentation_mode { CompositorThread::PresentToUI {} };
Optional<i32> presented_bitmap_id_awaiting_ack;
bool is_rasterizing { false };
bool needs_present { false };
Gfx::IntRect pending_viewport_rect;
bool has_deferred_async_scroll_present { false };
Gfx::IntRect deferred_async_scroll_present_viewport_rect;
u64 submitted_frame_id { 0 };
u64 completed_frame_id { 0 };
Vector<AsyncScrollOffset> pending_async_scroll_offsets;
Vector<AsyncScrollOperationID> completed_async_scroll_operation_ids;
Atomic<AsyncScrollOperationID> next_async_scroll_operation_id { 0 };
Gfx::IntRect async_scrolling_viewport_rect;
Atomic<bool> has_async_scrolling_state { false };
Atomic<bool> can_accept_async_wheel_events { false };
u64 wheel_event_listener_state_generation { 0 };
WheelRoutingAdmission wheel_routing_admission { WheelRoutingAdmission::NoAsyncScrollingState };
};
void register_context(CompositorContextId context_id, Optional<u64> page_id, CompositorThread::PagePresentationRegistration page_presentation_registration)
{
Sync::MutexLocker const locker { m_mutex };
VERIFY(!m_contexts.contains(context_id));
if (page_presentation_registration == CompositorThread::PagePresentationRegistration::Yes) {
VERIFY(page_id.has_value());
VERIFY(context_id == compositor_context_id_for_page(*page_id));
} else {
VERIFY(!page_id.has_value());
VERIFY(!is_page_presenting_context_id(context_id));
}
m_contexts.set(context_id, adopt_ref(*new ContextState(page_id, page_presentation_registration)));
}
void unregister_context(CompositorContextId context_id)
{
Sync::MutexLocker const locker { m_mutex };
m_contexts.remove(context_id);
}
RefPtr<ContextState> context_state(CompositorContextId context_id)
{
Sync::MutexLocker const locker { m_mutex };
return m_contexts.get(context_id).value_or(nullptr);
}
void stop_presenting_to_client(CompositorContextId context_id)
{
Sync::MutexLocker const locker { m_mutex };
if (auto context = m_contexts.get(context_id).value_or(nullptr))
context->presents_to_client = false;
}
bool context_presents_to_client(CompositorContextId context_id) const
{
Sync::MutexLocker const locker { m_mutex };
auto context = m_contexts.get(context_id).value_or(nullptr);
return context && context->presents_to_client;
}
void set_presentation_mode(CompositorContextId context_id, CompositorThread::PresentationMode mode)
{
Sync::MutexLocker const locker { m_mutex };
if (auto context = m_contexts.get(context_id).value_or(nullptr))
context->presentation_mode = move(mode);
}
void exit()
{
Sync::MutexLocker const locker { m_mutex };
m_exit = true;
m_command_ready.signal();
m_frame_completed.broadcast();
}
void enqueue_command(CompositorContextId context_id, CompositorCommand&& command)
{
Sync::MutexLocker const locker { m_mutex };
m_command_queue.enqueue({ context_id, move(command) });
if constexpr (COMPOSITOR_DEBUG) {
auto context = m_contexts.get(context_id).value_or(nullptr);
dbgln("[Compositor] Compositor command queued (queue_size={}, awaiting_bitmap={}, needs_present={}, deferred_async_present={})",
m_command_queue.size(),
context ? context->presented_bitmap_id_awaiting_ack.value_or(-1) : -1,
context ? context->needs_present : false,
context ? context->has_deferred_async_scroll_present : false);
}
m_command_ready.signal();
}
u64 set_needs_present(CompositorContextId context_id, Gfx::IntRect viewport_rect)
{
Sync::MutexLocker const locker { m_mutex };
auto context_state = m_contexts.get(context_id);
if (!context_state.has_value())
return 0;
auto& context = *context_state.value();
context.needs_present = true;
context.pending_viewport_rect = viewport_rect;
context.submitted_frame_id++;
m_command_ready.signal();
return context.submitted_frame_id;
}
void mark_frame_complete(ContextState& context, u64 frame_id)
{
Sync::MutexLocker const locker { m_mutex };
context.completed_frame_id = frame_id;
m_frame_completed.broadcast();
}
void wait_for_frame(CompositorContextId context_id, u64 frame_id)
{
Sync::MutexLocker const locker { m_mutex };
auto context_state = m_contexts.get(context_id);
if (!context_state.has_value())
return;
auto& context = *context_state.value();
while (context.completed_frame_id < frame_id && !m_exit)
m_frame_completed.wait();
}
CompositorThread::PendingAsyncScrollUpdates take_pending_async_scroll_updates(CompositorContextId context_id)
{
Sync::MutexLocker const locker { m_mutex };
CompositorThread::PendingAsyncScrollUpdates updates;
if (auto context = m_contexts.get(context_id).value_or(nullptr)) {
AK::swap(updates.scroll_offsets, context->pending_async_scroll_offsets);
AK::swap(updates.completed_operation_ids, context->completed_async_scroll_operation_ids);
}
return updates;
}
bool should_defer_async_scroll_offset_adoption(CompositorContextId context_id) const
{
Sync::MutexLocker const locker { m_mutex };
auto context = m_contexts.get(context_id).value_or(nullptr);
if (!context)
return false;
return !context->pending_async_scroll_offsets.is_empty()
&& context->is_rasterizing;
}
bool should_defer_main_thread_present_for_async_scroll(CompositorContextId context_id) const
{
Sync::MutexLocker const locker { m_mutex };
auto context = m_contexts.get(context_id).value_or(nullptr);
if (!context)
return false;
return !context->pending_async_scroll_offsets.is_empty()
&& (context->is_rasterizing || context->has_deferred_async_scroll_present || has_presented_bitmap_awaiting_ack_while_locked(*context));
}
void invalidate_wheel_event_listener_state(CompositorContextId context_id, u64 generation)
{
Sync::MutexLocker const locker { m_mutex };
auto context = m_contexts.get(context_id).value_or(nullptr);
if (!context)
return;
context->wheel_event_listener_state_generation = max(context->wheel_event_listener_state_generation, generation);
context->wheel_routing_admission = WheelRoutingAdmission::StaleWheelEventListeners;
context->can_accept_async_wheel_events = false;
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Invalidated compositor wheel listener state (generation={})", generation);
}
struct WheelTarget {
Optional<AsyncScrollNodeID> node_id;
bool blocked_by_main_thread_region { false };
bool blocked_by_wheel_event_region { false };
};
WheelTarget hit_test_scroll_node_for_wheel(ContextState& context, Gfx::FloatPoint position, Gfx::FloatPoint delta) const
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
auto scroll_target = context.async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta);
return {
.node_id = scroll_target.node_id,
.blocked_by_main_thread_region = scroll_target.blocked_by_main_thread_region,
.blocked_by_wheel_event_region = scroll_target.blocked_by_wheel_event_region,
};
}
CompositorThread::AsyncScrollEnqueueResult enqueue_async_scroll_by(CompositorContextId context_id, UniqueNodeID expected_document_id, Gfx::FloatPoint position,
Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, CompositorThread::AsyncScrollOperationTracking operation_tracking)
{
auto context_state = this->context_state(context_id);
if (!context_state)
return {};
auto& context = *context_state;
if (!context.can_accept_async_wheel_events.load()) {
auto wheel_routing_admission = [&] {
Sync::MutexLocker const locker { m_mutex };
return context.wheel_routing_admission;
}();
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: compositor cannot accept async wheel events ({})",
wheel_routing_admission_to_string(wheel_routing_admission));
return {};
}
auto scroll_target = hit_test_scroll_node_for_wheel(context, position, delta_in_device_pixels);
if (scroll_target.blocked_by_main_thread_region) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: main-thread wheel region at {},{} device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return {};
}
if (scroll_target.blocked_by_wheel_event_region) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: blocking wheel event region at {},{} device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return {};
}
if (!scroll_target.node_id.has_value()) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: no wheel target at {},{} for device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return {};
}
if (scroll_target.node_id->document_id != expected_document_id) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: stale wheel target at {},{} for current document",
position.x(), position.y());
return {};
}
Optional<AsyncScrollOperationID> operation_id;
if (operation_tracking == CompositorThread::AsyncScrollOperationTracking::Yes)
operation_id = next_async_scroll_operation_id(context);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor accepted main-thread async scroll enqueue at {},{} device delta {},{} for scroll node {} viewport={}x{} at {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y(), scroll_target.node_id->scroll_frame_index.value(), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
enqueue_command(context_id, AsyncScrollByCommand { position, delta_in_device_pixels, viewport_rect, *scroll_target.node_id, operation_id });
return { true, operation_id };
}
bool enqueue_async_scroll_by(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels)
{
auto context_id = compositor_context_id_for_page(page_id);
auto context_state = this->context_state(context_id);
if (!context_state)
return false;
auto& context = *context_state;
if (!context.presents_to_client)
return false;
if (!context.can_accept_async_wheel_events.load()) {
auto wheel_routing_admission = [&] {
Sync::MutexLocker const locker { m_mutex };
return context.wheel_routing_admission;
}();
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: compositor cannot accept async wheel events ({})",
wheel_routing_admission_to_string(wheel_routing_admission));
return false;
}
auto viewport_rect = [&] {
Sync::MutexLocker const locker { m_mutex };
return context.async_scrolling_viewport_rect;
}();
auto scroll_target = hit_test_scroll_node_for_wheel(context, position, delta_in_device_pixels);
if (scroll_target.blocked_by_main_thread_region) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: main-thread wheel region at {},{} device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return false;
}
if (scroll_target.blocked_by_wheel_event_region) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: blocking wheel event region at {},{} device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return false;
}
if (!scroll_target.node_id.has_value()) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rejecting async scroll enqueue: no wheel target at {},{} for device delta {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y());
return false;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor accepted async scroll enqueue at {},{} device delta {},{} for scroll node {} viewport={}x{} at {},{}",
position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y(), scroll_target.node_id->scroll_frame_index.value(), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
enqueue_command(context_id, AsyncScrollByCommand { position, delta_in_device_pixels, viewport_rect, *scroll_target.node_id, {} });
return true;
}
Optional<size_t> hit_test_viewport_scrollbar(ContextState& context, Gfx::FloatPoint position) const
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
for (size_t i = 0; i < context.viewport_scrollbars.size(); ++i) {
auto const& scrollbar = context.viewport_scrollbars[i];
auto scroll_offset = context.async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, context.cached_scroll_state_snapshot);
if (!scroll_offset.has_value())
continue;
if (scrollbar_hit_rect(scrollbar, *scroll_offset).to_type<float>().contains(position))
return i;
}
return {};
}
bool handle_viewport_scrollbar_mouse_event(u64 page_id, MouseEvent const& event)
{
auto context_id = compositor_context_id_for_page(page_id);
auto context_state = this->context_state(context_id);
if (!context_state)
return false;
auto& context = *context_state;
if (!context.presents_to_client)
return false;
auto position = Gfx::FloatPoint {
static_cast<float>(event.position.x().value()),
static_cast<float>(event.position.y().value()),
};
auto primary_position_for_scrollbar = [&](ViewportScrollbar const& scrollbar) {
return position.primary_offset_for_orientation(orientation_for_scrollbar(scrollbar));
};
switch (event.type) {
case MouseEvent::Type::MouseDown: {
if (event.button != UIEvents::MouseButton::Primary)
return false;
Optional<size_t> scrollbar_index;
float thumb_grab_position = 0;
float primary_position = 0;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
for (size_t i = 0; i < context.viewport_scrollbars.size(); ++i) {
auto const& scrollbar = context.viewport_scrollbars[i];
auto scroll_offset = context.async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, context.cached_scroll_state_snapshot);
if (!scroll_offset.has_value())
continue;
auto expanded = context.hovered_viewport_scrollbar_index == i || context.captured_viewport_scrollbar_index == i;
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = translated_thumb_rect(scrollbar, *scroll_offset, expanded);
primary_position = primary_position_for_scrollbar(scrollbar);
if (thumb_rect.to_type<float>().contains(position)) {
thumb_grab_position = primary_position - static_cast<float>(thumb_rect.primary_offset_for_orientation(orientation));
scrollbar_index = i;
break;
}
if (scrollbar_hit_rect(scrollbar, *scroll_offset).to_type<float>().contains(position)) {
auto gutter_rect = scrollbar_gutter_rect(scrollbar, true);
auto thumb_size = static_cast<float>(thumb_rect.primary_size_for_orientation(orientation));
auto gutter_start = static_cast<float>(gutter_rect.primary_offset_for_orientation(orientation));
auto gutter_size = static_cast<float>(gutter_rect.primary_size_for_orientation(orientation));
auto offset_relative_to_gutter = primary_position - gutter_start;
thumb_grab_position = max(min(offset_relative_to_gutter, thumb_size / 2), offset_relative_to_gutter - gutter_size + thumb_size);
scrollbar_index = i;
break;
}
}
if (!scrollbar_index.has_value())
return false;
context.captured_viewport_scrollbar_index = *scrollbar_index;
context.hovered_viewport_scrollbar_index = *scrollbar_index;
context.viewport_scrollbar_thumb_grab_position = thumb_grab_position;
}
schedule_viewport_scrollbar_present(context);
enqueue_command(context_id, ViewportScrollbarDragCommand { *scrollbar_index, primary_position, thumb_grab_position });
return true;
}
case MouseEvent::Type::MouseMove: {
Optional<size_t> scrollbar_index;
float thumb_grab_position = 0;
float primary_position = 0;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
if (context.captured_viewport_scrollbar_index.has_value()) {
scrollbar_index = *context.captured_viewport_scrollbar_index;
thumb_grab_position = context.viewport_scrollbar_thumb_grab_position;
if (*scrollbar_index >= context.viewport_scrollbars.size()) {
context.captured_viewport_scrollbar_index.clear();
return false;
}
primary_position = primary_position_for_scrollbar(context.viewport_scrollbars[*scrollbar_index]);
}
}
if (scrollbar_index.has_value()) {
enqueue_command(context_id, ViewportScrollbarDragCommand { *scrollbar_index, primary_position, thumb_grab_position });
return true;
}
auto hovered_scrollbar_index = hit_test_viewport_scrollbar(context, position);
set_hovered_viewport_scrollbar(context, hovered_scrollbar_index);
return hovered_scrollbar_index.has_value();
}
case MouseEvent::Type::MouseUp: {
Optional<size_t> scrollbar_index;
float thumb_grab_position = 0;
float primary_position = 0;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
if (!context.captured_viewport_scrollbar_index.has_value())
return false;
scrollbar_index = *context.captured_viewport_scrollbar_index;
thumb_grab_position = context.viewport_scrollbar_thumb_grab_position;
if (*scrollbar_index >= context.viewport_scrollbars.size()) {
context.captured_viewport_scrollbar_index.clear();
return false;
}
primary_position = primary_position_for_scrollbar(context.viewport_scrollbars[*scrollbar_index]);
context.captured_viewport_scrollbar_index.clear();
}
schedule_viewport_scrollbar_present(context);
enqueue_command(context_id, ViewportScrollbarDragCommand { *scrollbar_index, primary_position, thumb_grab_position });
return true;
}
case MouseEvent::Type::MouseLeave: {
auto has_capture = [&] {
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
return context.captured_viewport_scrollbar_index.has_value();
}();
set_hovered_viewport_scrollbar(context, {});
return has_capture;
}
case MouseEvent::Type::MouseWheel:
return false;
}
VERIFY_NOT_REACHED();
}
void compositor_loop(DisplayListPlayerType display_list_player_type)
{
initialize_skia_player(display_list_player_type);
while (true) {
{
Sync::MutexLocker const locker { m_mutex };
while (m_command_queue.is_empty() && !has_presentable_work() && !m_exit) {
m_command_ready.wait();
}
if (m_exit)
break;
}
// Drain autoreleased Objective-C objects created by Metal/Skia each iteration,
// since this background thread has no autorelease pool.
Core::ScopedAutoreleasePool autorelease_pool;
while (true) {
auto command = [this]() -> Optional<CompositorCommandEnvelope> {
Sync::MutexLocker const locker { m_mutex };
if (m_command_queue.is_empty())
return {};
auto command = m_command_queue.dequeue();
return command;
}();
if (!command.has_value())
break;
auto context_state = this->context_state(command->context_id);
if (!context_state)
continue;
auto& context = *context_state;
bool should_yield_to_async_scroll_present = false;
command->command.visit(
[this, &context](UpdateDisplayListCommand& cmd) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor processing display list update (deferred_async_present={})",
context.has_deferred_async_scroll_present);
context.display_list_resource_storage.apply_transaction(move(cmd.resource_transaction));
context.cached_display_list = move(cmd.display_list);
context.cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
auto async_scrolling_state = async_scrolling_state_from_display_list(*context.cached_display_list);
auto async_scrolling_viewport_rect = async_scrolling_state.viewport_rect;
auto const wheel_event_listener_state_generation = async_scrolling_state.wheel_event_listener_state_generation;
auto wheel_routing_admission = wheel_routing_admission_for(async_scrolling_state);
{
Sync::MutexLocker const locker { m_mutex };
if (wheel_event_listener_state_generation < context.wheel_event_listener_state_generation)
wheel_routing_admission = WheelRoutingAdmission::StaleWheelEventListeners;
else
context.wheel_event_listener_state_generation = wheel_event_listener_state_generation;
context.wheel_routing_admission = wheel_routing_admission;
}
context.can_accept_async_wheel_events = wheel_routing_admission == WheelRoutingAdmission::Accepted;
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor wheel routing admission: {} (scroll_nodes={}, sticky_areas={}, blocking_regions={})",
wheel_routing_admission_to_string(wheel_routing_admission),
async_scrolling_state.scroll_nodes.size(),
async_scrolling_state.sticky_areas.size(),
async_scrolling_state.blocking_wheel_event_regions.size());
auto pending_async_scroll_offsets = [this, &context] {
Sync::MutexLocker const locker { m_mutex };
return context.pending_async_scroll_offsets;
}();
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
auto hovered_scrollbar_identity = viewport_scrollbar_identity_at(context.viewport_scrollbars, context.hovered_viewport_scrollbar_index);
auto captured_scrollbar_identity = viewport_scrollbar_identity_at(context.viewport_scrollbars, context.captured_viewport_scrollbar_index);
context.viewport_scrollbars = move(async_scrolling_state.viewport_scrollbars);
context.hovered_viewport_scrollbar_index = hovered_scrollbar_identity.has_value() ? find_viewport_scrollbar_index(context.viewport_scrollbars, *hovered_scrollbar_identity) : Optional<size_t> {};
context.captured_viewport_scrollbar_index = captured_scrollbar_identity.has_value() ? find_viewport_scrollbar_index(context.viewport_scrollbars, *captured_scrollbar_identity) : Optional<size_t> {};
context.async_scroll_tree.set_state(move(async_scrolling_state));
if (!pending_async_scroll_offsets.is_empty()) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Reapplying {} pending async scroll offset(s) to display list update",
pending_async_scroll_offsets.size());
if (auto viewport_scroll_offset = reapply_pending_async_scroll_offsets(context, pending_async_scroll_offsets); viewport_scroll_offset.has_value())
async_scrolling_viewport_rect.set_location(viewport_scroll_offset->to_type<int>());
}
context.async_scroll_tree.rebuild_wheel_hit_test_targets(context.cached_display_list, context.cached_scroll_state_snapshot);
}
{
Sync::MutexLocker const locker { m_mutex };
context.async_scrolling_viewport_rect = async_scrolling_viewport_rect;
}
context.has_async_scrolling_state = true;
},
[this, &context, &should_yield_to_async_scroll_present](AsyncScrollByCommand& cmd) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor processing async scroll command at {},{} device delta {},{} for scroll node {} (deferred_async_present={})",
cmd.position.x(), cmd.position.y(), cmd.delta_in_device_pixels.x(), cmd.delta_in_device_pixels.y(), cmd.scroll_target.scroll_frame_index.value(), context.has_deferred_async_scroll_present);
auto async_scroll_viewport_rect = cmd.viewport_rect;
Vector<AsyncScrollOffset> scroll_offsets;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
scroll_offsets = context.async_scroll_tree.apply_scroll_delta(cmd.scroll_target, cmd.delta_in_device_pixels, context.cached_scroll_state_snapshot);
if (scroll_offsets.is_empty()) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping async scroll command: scroll tree consumed no delta");
// The operation produced no scroll offset (e.g. already at the scroll boundary), so it
// cannot be reported through the pending-offset path; report its completion separately.
complete_async_scroll_operation(context, cmd.operation_id);
return;
}
context.async_scroll_tree.rebuild_wheel_hit_test_targets(context.cached_display_list, context.cached_scroll_state_snapshot);
}
if (auto viewport_scroll_offset = viewport_scroll_offset_from(scroll_offsets); viewport_scroll_offset.has_value())
async_scroll_viewport_rect.set_location(viewport_scroll_offset->to_type<int>());
store_pending_async_scroll_offsets(context, scroll_offsets, cmd.operation_id);
{
Sync::MutexLocker const locker { m_mutex };
context.async_scrolling_viewport_rect = async_scroll_viewport_rect;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Stored {} pending async scroll offset(s)",
scroll_offsets.size());
invoke_on_main_thread([] {
HTML::main_thread_event_loop().queue_task_to_update_the_rendering();
});
{
Sync::MutexLocker const locker { m_mutex };
context.has_deferred_async_scroll_present = true;
context.deferred_async_scroll_present_viewport_rect = async_scroll_viewport_rect;
}
should_yield_to_async_scroll_present = can_present_deferred_async_scroll(context);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor async scroll command complete (yield_to_present={})",
should_yield_to_async_scroll_present);
},
[this, &context, &should_yield_to_async_scroll_present](ViewportScrollbarDragCommand& cmd) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor processing viewport scrollbar drag (index={}, position={}, grab={})",
cmd.scrollbar_index, cmd.primary_position, cmd.thumb_grab_position);
if (apply_viewport_scrollbar_drag(context, cmd.scrollbar_index, cmd.primary_position, cmd.thumb_grab_position)) {
should_yield_to_async_scroll_present = can_present_deferred_async_scroll(context);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor viewport scrollbar drag complete (yield_to_present={})",
should_yield_to_async_scroll_present);
}
},
[this, &context](UpdateScrollStateCommand& cmd) {
context.cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
if (context.has_async_scrolling_state) {
auto pending_async_scroll_offsets = [this, &context] {
Sync::MutexLocker const locker { m_mutex };
return context.pending_async_scroll_offsets;
}();
Optional<Gfx::FloatPoint> reconciled_viewport_scroll_offset;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
// A main-thread scroll-state update can be older than compositor-side async
// scrolling. Preserve compositor-visible offsets in the fresh snapshot before
// rebuilding hit-test data from it.
reconciled_viewport_scroll_offset = reapply_pending_async_scroll_offsets(context, pending_async_scroll_offsets);
context.async_scroll_tree.rebuild_wheel_hit_test_targets(context.cached_display_list, context.cached_scroll_state_snapshot);
}
if (reconciled_viewport_scroll_offset.has_value()) {
Sync::MutexLocker const mutex_locker { m_mutex };
auto reconciled_viewport_rect = context.async_scrolling_viewport_rect;
reconciled_viewport_rect.set_location(reconciled_viewport_scroll_offset->to_type<int>());
context.async_scrolling_viewport_rect = reconciled_viewport_rect;
}
}
},
[&context](UpdateCompositorSurfaceCommand& cmd) {
context.display_list_resource_storage.update_compositor_surface(cmd.surface_id, move(cmd.shared_image));
},
[&context](ClearCompositorSurfaceCommand& cmd) {
context.display_list_resource_storage.clear_compositor_surface(cmd.surface_id);
},
[this, &context](ViewportSizeUpdatedCommand& cmd) {
auto allocation = context.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 (context.has_async_scrolling_state) {
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());
}
if (auto publication = context.backing_store_manager.allocate_backing_stores(*allocation, m_skia_backend_context, context.presents_to_client); publication.has_value())
publish_backing_store_pair(context, publication.release_value());
},
[this, &context](ScreenshotCommand& cmd) {
if (!context.cached_display_list)
return;
m_skia_player->execute(*context.cached_display_list, context.display_list_resource_storage, context.cached_scroll_state_snapshot, *cmd.target_surface);
paint_viewport_scrollbar_overlay(context, *cmd.target_surface);
flush_surface(*cmd.target_surface);
if (cmd.callback) {
invoke_on_main_thread([callback = move(cmd.callback)]() mutable {
callback();
});
}
});
if (m_exit)
break;
if (should_yield_to_async_scroll_present) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor yielding command drain to present async scroll");
break;
}
}
if (m_exit)
break;
bool should_present = false;
RefPtr<ContextState> present_context_state;
Gfx::IntRect viewport_rect;
u64 presenting_frame_id = 0;
bool should_present_deferred_async_scroll = false;
Gfx::IntRect deferred_async_scroll_viewport_rect;
Optional<u64> deferred_async_scroll_presenting_frame_id;
{
Sync::MutexLocker const locker { m_mutex };
for (auto& [_, context] : m_contexts) {
if (context->has_deferred_async_scroll_present && !has_presented_bitmap_awaiting_ack_while_locked(*context)) {
present_context_state = context;
should_present_deferred_async_scroll = true;
deferred_async_scroll_viewport_rect = context->deferred_async_scroll_present_viewport_rect;
context->has_deferred_async_scroll_present = false;
if (context->needs_present) {
deferred_async_scroll_presenting_frame_id = context->submitted_frame_id;
context->needs_present = false;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor selected deferred async present (pending_main_thread_present={})",
deferred_async_scroll_presenting_frame_id.has_value());
break;
}
if (context->needs_present && !has_presented_bitmap_awaiting_ack_while_locked(*context)) {
present_context_state = context;
should_present = true;
viewport_rect = context->pending_viewport_rect;
presenting_frame_id = context->submitted_frame_id;
context->needs_present = false;
break;
}
}
}
if (should_present_deferred_async_scroll)
present_frame(*present_context_state, deferred_async_scroll_viewport_rect, deferred_async_scroll_presenting_frame_id, PresentFrameDelivery::AsyncScroll);
else if (should_present)
present_frame(*present_context_state, viewport_rect, presenting_frame_id, PresentFrameDelivery::MainThread);
}
}
private:
enum class PresentFrameDelivery {
MainThread,
AsyncScroll,
};
bool has_presented_bitmap_awaiting_ack_while_locked(ContextState const& context) const
{
return context.presented_bitmap_id_awaiting_ack.has_value();
}
AsyncScrollOperationID next_async_scroll_operation_id(ContextState& context)
{
return context.next_async_scroll_operation_id.fetch_add(1) + 1;
}
void complete_async_scroll_operation(ContextState& context, Optional<AsyncScrollOperationID> operation_id)
{
if (!operation_id.has_value())
return;
{
Sync::MutexLocker const locker { m_mutex };
context.completed_async_scroll_operation_ids.append(*operation_id);
}
// No scroll offset was produced, so nothing else will wake the main thread; schedule a rendering update so it
// drains the completed operation and resolves the awaiting promise.
invoke_on_main_thread([] {
HTML::main_thread_event_loop().queue_task_to_update_the_rendering();
});
}
void queue_rendering_update_if_async_scroll_updates_pending(ContextState& context)
{
auto has_pending_updates = [this, &context] {
Sync::MutexLocker const locker { m_mutex };
return !context.pending_async_scroll_offsets.is_empty()
|| !context.completed_async_scroll_operation_ids.is_empty();
}();
if (!has_pending_updates)
return;
invoke_on_main_thread([] {
HTML::main_thread_event_loop().queue_task_to_update_the_rendering();
});
}
void store_pending_async_scroll_offsets(ContextState& context, Vector<AsyncScrollOffset> const& scroll_offsets, Optional<AsyncScrollOperationID> operation_id = {})
{
Sync::MutexLocker const locker { m_mutex };
for (auto const& scroll_offset : scroll_offsets)
set_or_append_pending_scroll_offset(context.pending_async_scroll_offsets, scroll_offset);
if (operation_id.has_value())
context.completed_async_scroll_operation_ids.append(*operation_id);
}
Optional<Gfx::FloatPoint> reapply_pending_async_scroll_offsets(ContextState& context, Vector<AsyncScrollOffset> const& pending_scroll_offsets)
{
Optional<Gfx::FloatPoint> viewport_scroll_offset;
for (auto const& pending_scroll_offset : pending_scroll_offsets) {
auto node_id = context.async_scroll_tree.scroll_node_id_for_stable_id(pending_scroll_offset.stable_node_id);
if (!node_id.has_value())
continue;
auto current_scroll_offset = context.async_scroll_tree.scroll_offset_for_node(*node_id, context.cached_scroll_state_snapshot);
if (!current_scroll_offset.has_value())
continue;
// Reapplying pending async offsets is a restoration step for a freshly received
// main-thread snapshot. The pending offset is already the compositor-visible
// position, so applying the unadopted delta here would compound it.
auto reconciled_scroll_offset = context.async_scroll_tree.set_scroll_offset(*node_id, pending_scroll_offset.compositor_scroll_offset, context.cached_scroll_state_snapshot);
if (reconciled_scroll_offset.has_value() && context.async_scroll_tree.scroll_node_is_viewport(*node_id))
viewport_scroll_offset = *reconciled_scroll_offset;
}
return viewport_scroll_offset;
}
static Optional<Gfx::FloatPoint> viewport_scroll_offset_from(Vector<AsyncScrollOffset> const& scroll_offsets)
{
for (auto const& scroll_offset : scroll_offsets) {
if (scroll_offset.stable_node_id.kind == AsyncScrollNodeKind::Viewport)
return scroll_offset.compositor_scroll_offset;
}
return {};
}
void paint_viewport_scrollbar_overlay(ContextState& context, Gfx::PaintingSurface& surface)
{
Vector<ViewportScrollbar> scrollbars;
Optional<size_t> hovered_scrollbar_index;
Optional<size_t> captured_scrollbar_index;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
scrollbars = context.viewport_scrollbars;
hovered_scrollbar_index = context.hovered_viewport_scrollbar_index;
captured_scrollbar_index = context.captured_viewport_scrollbar_index;
}
paint_viewport_scrollbars(surface, scrollbars, context.cached_scroll_state_snapshot, hovered_scrollbar_index, captured_scrollbar_index);
}
void schedule_viewport_scrollbar_present(ContextState& context)
{
Sync::MutexLocker const locker { m_mutex };
if (context.async_scrolling_viewport_rect.is_empty())
return;
context.has_deferred_async_scroll_present = true;
context.deferred_async_scroll_present_viewport_rect = context.async_scrolling_viewport_rect;
m_command_ready.signal();
}
void set_hovered_viewport_scrollbar(ContextState& context, Optional<size_t> scrollbar_index)
{
bool changed = false;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
if (context.hovered_viewport_scrollbar_index == scrollbar_index)
return;
changed = true;
context.hovered_viewport_scrollbar_index = scrollbar_index;
}
if (changed)
schedule_viewport_scrollbar_present(context);
}
bool apply_viewport_scrollbar_drag(ContextState& context, size_t scrollbar_index, float primary_position, float thumb_grab_position)
{
Vector<AsyncScrollOffset> scroll_offsets;
{
Sync::MutexLocker const locker { context.async_scroll_tree_mutex };
if (scrollbar_index >= context.viewport_scrollbars.size())
return false;
auto const& scrollbar = context.viewport_scrollbars[scrollbar_index];
auto expanded = context.hovered_viewport_scrollbar_index == scrollbar_index || context.captured_viewport_scrollbar_index == scrollbar_index;
auto scroll_size = scrollbar_scroll_size(scrollbar, expanded);
if (scroll_size == 0)
return false;
auto current_scroll_offset = context.async_scroll_tree.scroll_offset_for_node(scrollbar.scroll_node_id, context.cached_scroll_state_snapshot);
if (!current_scroll_offset.has_value())
return false;
auto orientation = orientation_for_scrollbar(scrollbar);
auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect;
auto min_thumb_position = static_cast<float>(thumb_rect.primary_offset_for_orientation(orientation));
auto max_thumb_position = min_thumb_position + scrollbar.max_scroll_offset * static_cast<float>(scroll_size);
auto target_thumb_position = AK::clamp(primary_position - thumb_grab_position, min_thumb_position, max_thumb_position);
auto target_scroll_offset = (target_thumb_position - min_thumb_position) / static_cast<float>(scroll_size);
Gfx::FloatPoint delta;
delta.set_primary_offset_for_orientation(orientation, target_scroll_offset - current_scroll_offset->primary_offset_for_orientation(orientation));
if (delta.x() == 0 && delta.y() == 0)
return false;
scroll_offsets = context.async_scroll_tree.apply_scroll_delta(scrollbar.scroll_node_id, delta, context.cached_scroll_state_snapshot);
if (scroll_offsets.is_empty())
return false;
context.async_scroll_tree.rebuild_wheel_hit_test_targets(context.cached_display_list, context.cached_scroll_state_snapshot);
}
auto scroll_offset = viewport_scroll_offset_from(scroll_offsets);
if (!scroll_offset.has_value())
return false;
store_pending_async_scroll_offsets(context, scroll_offsets);
Gfx::IntRect async_scroll_viewport_rect;
{
Sync::MutexLocker const locker { m_mutex };
async_scroll_viewport_rect = context.async_scrolling_viewport_rect;
async_scroll_viewport_rect.set_location(scroll_offset->to_type<int>());
context.async_scrolling_viewport_rect = async_scroll_viewport_rect;
context.has_deferred_async_scroll_present = true;
context.deferred_async_scroll_present_viewport_rect = async_scroll_viewport_rect;
}
invoke_on_main_thread([] {
HTML::main_thread_event_loop().queue_task_to_update_the_rendering();
});
return true;
}
bool has_presentable_work() const
{
for (auto const& [_, context] : m_contexts) {
if ((context->has_deferred_async_scroll_present || context->needs_present)
&& !has_presented_bitmap_awaiting_ack_while_locked(*context)) {
return true;
}
}
return false;
}
bool can_present_deferred_async_scroll(ContextState& context) const
{
Sync::MutexLocker const locker { m_mutex };
return context.has_deferred_async_scroll_present && !has_presented_bitmap_awaiting_ack_while_locked(context);
}
void present_frame(ContextState& context, Gfx::IntRect viewport_rect, Optional<u64> presenting_frame_id = {}, PresentFrameDelivery delivery = PresentFrameDelivery::MainThread)
{
auto delivery_name = delivery == PresentFrameDelivery::AsyncScroll ? "compositor-thread"sv : "main-thread"sv;
{
Sync::MutexLocker const locker { m_mutex };
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Begin {} present (frame={}, awaiting_bitmap={}, viewport={}x{} at {},{})",
delivery_name, presenting_frame_id.value_or(0), context.presented_bitmap_id_awaiting_ack.value_or(-1), viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
if (delivery == PresentFrameDelivery::AsyncScroll && has_presented_bitmap_awaiting_ack_while_locked(context)) {
context.has_deferred_async_scroll_present = true;
context.deferred_async_scroll_present_viewport_rect = viewport_rect;
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Deferring async scroll present until bitmap {} is ready to paint",
context.presented_bitmap_id_awaiting_ack.value());
return;
}
VERIFY(!has_presented_bitmap_awaiting_ack_while_locked(context));
if (m_exit)
return;
context.is_rasterizing = true;
}
auto presentation_mode = [&] {
Sync::MutexLocker const locker { m_mutex };
return context.presentation_mode;
}();
if (context.cached_display_list && context.backing_store_manager.is_valid()) {
auto should_clear_back_store = presentation_mode.visit(
[](CompositorThread::PresentToUI) { return false; },
[](CompositorThread::PublishToCompositorSurface const&) { return true; });
auto& back_store = context.backing_store_manager.back_store();
if (should_clear_back_store) {
// Embedded navigables leave their PaintConfig canvas unfilled, so double-buffered back stores must be
// cleared before repainting.
back_store.canvas().clear(SK_ColorTRANSPARENT);
}
m_skia_player->execute(*context.cached_display_list, context.display_list_resource_storage, context.cached_scroll_state_snapshot, back_store);
paint_viewport_scrollbar_overlay(context, back_store);
flush_surface(back_store);
i32 rendered_bitmap_id = context.backing_store_manager.back_bitmap_id();
context.backing_store_manager.swap();
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Finished {} display-list replay into bitmap {}",
delivery_name, rendered_bitmap_id);
presentation_mode.visit(
[this, &context, viewport_rect, rendered_bitmap_id, delivery](CompositorThread::PresentToUI) {
if (context.presents_to_client) {
VERIFY(context.page_id.has_value());
finish_rasterizing(context, rendered_bitmap_id);
if (delivery == PresentFrameDelivery::AsyncScroll) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Finished async scroll raster into bitmap {}",
rendered_bitmap_id);
}
VERIFY(CompositorThread::present_frame_to_client(*context.page_id, viewport_rect, rendered_bitmap_id));
} else {
Sync::MutexLocker const locker { m_mutex };
context.is_rasterizing = false;
}
},
[this, &context](CompositorThread::PublishToCompositorSurface const& mode) {
{
Sync::MutexLocker const locker { m_mutex };
context.is_rasterizing = false;
}
if (context.has_async_scrolling_state)
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Publishing present to compositor surface");
auto& front_store = context.backing_store_manager.front_store();
VERIFY(CompositorThread::update_compositor_surface_for_context(
mode.target_context_id, mode.surface_id, front_store.snapshot_into_shared_image()));
});
} else {
{
Sync::MutexLocker const locker { m_mutex };
context.is_rasterizing = false;
}
if (context.has_async_scrolling_state) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Skipping {} present: cached_display_list={}, backing_stores_valid={}",
delivery_name, !!context.cached_display_list, context.backing_store_manager.is_valid());
}
}
if (presenting_frame_id.has_value())
mark_frame_complete(context, *presenting_frame_id);
queue_rendering_update_if_async_scroll_updates_pending(context);
}
void initialize_skia_player(DisplayListPlayerType display_list_player_type)
{
switch (display_list_player_type) {
case DisplayListPlayerType::SkiaGPUIfAvailable:
m_skia_backend_context = Gfx::SkiaBackendContext::create_independent_gpu_backend();
break;
case DisplayListPlayerType::SkiaCPU:
break;
}
m_skia_player = make<Painting::DisplayListPlayerSkia>(m_skia_backend_context);
}
void publish_backing_store_pair(ContextState& context, BackingStoreManager::Publication&& publication)
{
if (!context.presents_to_client)
return;
VERIFY(context.page_id.has_value());
VERIFY(CompositorThread::present_backing_stores_to_client(
*context.page_id, publication.front_bitmap_id, move(publication.front_shared_image), publication.back_bitmap_id, move(publication.back_shared_image)));
}
template<typename Invokee>
void invoke_on_main_thread(Invokee invokee)
{
if (m_exit)
return;
auto event_loop = m_main_thread_event_loop->take();
if (!event_loop)
return;
event_loop->deferred_invoke([self = NonnullRefPtr(*this), invokee = move(invokee)]() mutable {
invokee();
});
}
NonnullRefPtr<Core::WeakEventLoopReference> m_main_thread_event_loop;
mutable Sync::Mutex m_mutex;
mutable Sync::ConditionVariable m_command_ready { m_mutex };
Atomic<bool> m_exit { false };
Queue<CompositorCommandEnvelope> m_command_queue;
HashMap<CompositorContextId, NonnullRefPtr<ContextState>> m_contexts;
OwnPtr<Painting::DisplayListPlayerSkia> m_skia_player;
RefPtr<Gfx::SkiaBackendContext> m_skia_backend_context;
mutable Sync::ConditionVariable m_frame_completed { m_mutex };
public:
void finish_rasterizing(ContextState& context, i32 bitmap_id)
{
Sync::MutexLocker const locker { m_mutex };
context.is_rasterizing = false;
VERIFY(!context.presented_bitmap_id_awaiting_ack.has_value());
context.presented_bitmap_id_awaiting_ack = bitmap_id;
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Rasterized bitmap {} waiting for ready_to_paint",
bitmap_id);
}
void mark_presented_bitmap_ready_to_paint(u64 page_id, i32 bitmap_id)
{
auto context_state = this->context_state(compositor_context_id_for_page(page_id));
if (!context_state)
return;
auto& context = *context_state;
Sync::MutexLocker const locker { m_mutex };
if (!context.presents_to_client)
return;
if (context.presented_bitmap_id_awaiting_ack != bitmap_id) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Ignoring stale ready_to_paint for bitmap {} while awaiting bitmap {}",
bitmap_id, context.presented_bitmap_id_awaiting_ack.value_or(-1));
return;
}
context.presented_bitmap_id_awaiting_ack.clear();
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Compositor ready_to_paint released bitmap {}",
bitmap_id);
m_command_ready.signal();
}
};
struct FramePresentationState {
RefPtr<Core::WeakEventLoopReference> event_loop;
CompositorThread::BackingStorePresentationCallback backing_store_callback;
CompositorThread::FramePresentationCallback frame_callback;
};
static Sync::Mutex& compositor_presentation_state_mutex()
{
static NeverDestroyed<Sync::Mutex> mutex;
return *mutex;
}
static HashMap<CompositorContextId, NonnullRefPtr<CompositorThread::ThreadData>>& context_compositors()
{
static NeverDestroyed<HashMap<CompositorContextId, NonnullRefPtr<CompositorThread::ThreadData>>> compositors;
return *compositors;
}
static RefPtr<CompositorThread::ThreadData> thread_data_for_context(CompositorContextId context_id)
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto compositor = context_compositors().get(context_id);
if (!compositor.has_value())
return nullptr;
return compositor.value();
}
static RefPtr<CompositorThread::ThreadData> thread_data_for_page(u64 page_id)
{
return thread_data_for_context(compositor_context_id_for_page(page_id));
}
static bool page_context_presents_to_client(u64 page_id)
{
auto context_id = compositor_context_id_for_page(page_id);
auto thread_data = thread_data_for_context(context_id);
if (!thread_data)
return false;
return thread_data->context_presents_to_client(context_id);
}
static FramePresentationState& frame_presentation_state()
{
static NeverDestroyed<FramePresentationState> state;
return *state;
}
CompositorThread::Context::Context(NonnullRefPtr<ThreadData> thread_data, CompositorContextId context_id)
: m_thread_data(move(thread_data))
, m_context_id(context_id)
{
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);
});
}
CompositorThread::Context::~Context()
{
m_backing_store_shrink_timer->on_timeout = {};
m_backing_store_shrink_timer->stop();
m_backing_store_shrink_timer.clear();
m_thread_data->unregister_context(m_context_id);
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
VERIFY(context_compositors().remove(m_context_id));
}
}
CompositorThread::CompositorThread()
: m_thread_data(adopt_ref(*new ThreadData(Core::EventLoop::current_weak())))
{
}
CompositorThread::~CompositorThread()
{
m_thread_data->exit();
}
OwnPtr<CompositorThread::Context> CompositorThread::create_context(Optional<u64> page_id, PagePresentationRegistration page_presentation_registration)
{
CompositorContextId context_id;
if (page_presentation_registration == PagePresentationRegistration::Yes) {
VERIFY(page_id.has_value());
context_id = compositor_context_id_for_page(*page_id);
} else {
VERIFY(!page_id.has_value());
context_id = allocate_compositor_context_id();
VERIFY(!is_page_presenting_context_id(context_id));
}
m_thread_data->register_context(context_id, page_id, page_presentation_registration);
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
VERIFY(!context_compositors().contains(context_id));
context_compositors().set(context_id, m_thread_data);
}
return adopt_own(*new Context(m_thread_data, context_id));
}
bool CompositorThread::update_compositor_surface_for_context(CompositorContextId context_id, Painting::CompositorSurfaceId surface_id, Gfx::SharedImage&& shared_image)
{
auto thread_data = thread_data_for_context(context_id);
if (!thread_data) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping compositor surface {} update for unknown context {}",
surface_id, context_id);
return false;
}
thread_data->enqueue_command(context_id, UpdateCompositorSurfaceCommand { surface_id, move(shared_image) });
return true;
}
void CompositorThread::set_frame_presentation_callbacks(NonnullRefPtr<Core::WeakEventLoopReference> event_loop, BackingStorePresentationCallback backing_store_callback, FramePresentationCallback frame_callback)
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
state.event_loop = move(event_loop);
state.backing_store_callback = move(backing_store_callback);
state.frame_callback = move(frame_callback);
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Installed compositor presentation callbacks");
}
void CompositorThread::clear_frame_presentation_callbacks()
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
state.event_loop = nullptr;
state.backing_store_callback = {};
state.frame_callback = {};
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cleared compositor presentation callbacks");
}
bool CompositorThread::present_backing_stores_to_client(u64 page_id, i32 front_bitmap_id, Gfx::SharedImage&& front_shared_image, i32 back_bitmap_id, Gfx::SharedImage&& back_shared_image)
{
auto context_id = compositor_context_id_for_page(page_id);
auto thread_data = thread_data_for_context(context_id);
if (!thread_data || !thread_data->context_presents_to_client(context_id)) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present backing stores front={} back={} for page {}: no presenting context registered",
front_bitmap_id, back_bitmap_id, page_id);
return false;
}
RefPtr<Core::WeakEventLoopReference> event_loop_reference;
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
if (!state.backing_store_callback) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present backing stores front={} back={} for page {}: no presentation callback",
front_bitmap_id, back_bitmap_id, page_id);
return false;
}
event_loop_reference = state.event_loop;
}
if (!event_loop_reference) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present backing stores front={} back={} for page {}: no presentation event loop",
front_bitmap_id, back_bitmap_id, page_id);
return false;
}
auto event_loop = event_loop_reference->take();
if (!event_loop) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present backing stores front={} back={} for page {}: presentation event loop is gone",
front_bitmap_id, back_bitmap_id, page_id);
return false;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Queueing UI backing stores for page {} front={} back={}",
page_id, front_bitmap_id, back_bitmap_id);
event_loop->deferred_invoke([page_id, front_bitmap_id, front_shared_image = move(front_shared_image), back_bitmap_id, back_shared_image = move(back_shared_image)]() mutable {
if (!page_context_presents_to_client(page_id)) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping queued UI backing stores for page {} front={} back={}: page unregistered",
page_id, front_bitmap_id, back_bitmap_id);
return;
}
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
if (state.backing_store_callback) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Delivering UI backing stores for page {} front={} back={}",
page_id, front_bitmap_id, back_bitmap_id);
state.backing_store_callback(page_id, front_bitmap_id, move(front_shared_image), back_bitmap_id, move(back_shared_image));
} else {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping queued UI backing stores for page {} front={} back={}: callback cleared",
page_id, front_bitmap_id, back_bitmap_id);
}
});
return true;
}
bool CompositorThread::present_frame_to_client(u64 page_id, Gfx::IntRect const& viewport_rect, i32 bitmap_id)
{
auto context_id = compositor_context_id_for_page(page_id);
auto thread_data = thread_data_for_context(context_id);
if (!thread_data || !thread_data->context_presents_to_client(context_id)) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present bitmap {} for page {}: no presenting context registered",
bitmap_id, page_id);
return false;
}
RefPtr<Core::WeakEventLoopReference> event_loop_reference;
{
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
if (!state.frame_callback) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present bitmap {} for page {}: no presentation callback",
bitmap_id, page_id);
return false;
}
event_loop_reference = state.event_loop;
}
if (!event_loop_reference) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present bitmap {} for page {}: no presentation event loop",
bitmap_id, page_id);
return false;
}
auto event_loop = event_loop_reference->take();
if (!event_loop) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Cannot present bitmap {} for page {}: presentation event loop is gone",
bitmap_id, page_id);
return false;
}
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Queueing UI present for page {} bitmap {} viewport={}x{} at {},{}",
page_id, bitmap_id, viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
event_loop->deferred_invoke([page_id, viewport_rect, bitmap_id] {
if (!page_context_presents_to_client(page_id)) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping queued UI present for page {} bitmap {}: page unregistered",
page_id, bitmap_id);
return;
}
Sync::MutexLocker const locker { compositor_presentation_state_mutex() };
auto& state = frame_presentation_state();
if (state.frame_callback) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Delivering UI present for page {} bitmap {} viewport={}x{} at {},{}",
page_id, bitmap_id, viewport_rect.width(), viewport_rect.height(), viewport_rect.x(), viewport_rect.y());
state.frame_callback(page_id, viewport_rect, bitmap_id);
} else {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Dropping queued UI present for page {} bitmap {}: callback cleared",
page_id, bitmap_id);
}
});
return true;
}
void CompositorThread::presented_bitmap_ready_to_paint(u64 page_id, i32 bitmap_id)
{
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Received compositor ready_to_paint for page {} bitmap {}",
page_id, bitmap_id);
auto thread_data = thread_data_for_page(page_id);
if (!thread_data) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Ignoring compositor ready_to_paint for page {} bitmap {}: no presenting context registered",
page_id, bitmap_id);
return;
}
thread_data->mark_presented_bitmap_ready_to_paint(page_id, bitmap_id);
}
bool CompositorThread::async_scroll_by(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels)
{
auto thread_data = thread_data_for_page(page_id);
if (!thread_data) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Async scroll cannot scroll page {}: no presenting context registered", page_id);
return false;
}
return thread_data->enqueue_async_scroll_by(page_id, position, delta_in_device_pixels);
}
bool CompositorThread::handle_mouse_event(u64 page_id, MouseEvent const& event)
{
auto thread_data = thread_data_for_page(page_id);
if (!thread_data) {
dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Viewport scrollbar mouse event cannot be handled for page {}: no presenting context registered", page_id);
return false;
}
return thread_data->handle_viewport_scrollbar_mouse_event(page_id, event);
}
void CompositorThread::start(DisplayListPlayerType display_list_player_type)
{
m_thread = Threading::Thread::construct("Compositor"sv, [thread_data = m_thread_data, display_list_player_type] {
thread_data->compositor_loop(display_list_player_type);
return static_cast<intptr_t>(0);
});
m_thread->start();
m_thread->detach();
}
void CompositorThread::Context::set_presentation_mode(PresentationMode mode)
{
m_thread_data->set_presentation_mode(m_context_id, move(mode));
}
void CompositorThread::Context::stop_presenting_to_client()
{
m_thread_data->stop_presenting_to_client(m_context_id);
}
void CompositorThread::Context::update_display_list(
NonnullRefPtr<Painting::DisplayList> display_list,
Painting::DisplayListResourceTransaction&& resource_transaction,
Painting::ScrollStateSnapshot&& scroll_state_snapshot)
{
m_thread_data->enqueue_command(m_context_id, UpdateDisplayListCommand { move(display_list), move(resource_transaction), move(scroll_state_snapshot) });
}
void CompositorThread::Context::update_compositor_surface(Painting::CompositorSurfaceId surface_id, Gfx::SharedImage&& shared_image)
{
m_thread_data->enqueue_command(m_context_id, UpdateCompositorSurfaceCommand { surface_id, move(shared_image) });
}
void CompositorThread::Context::clear_compositor_surface(Painting::CompositorSurfaceId surface_id)
{
m_thread_data->enqueue_command(m_context_id, ClearCompositorSurfaceCommand { surface_id });
}
void CompositorThread::Context::invalidate_wheel_event_listener_state(u64 generation)
{
m_thread_data->invalidate_wheel_event_listener_state(m_context_id, generation);
}
CompositorThread::AsyncScrollEnqueueResult CompositorThread::Context::async_scroll_by(UniqueNodeID expected_document_id, Gfx::FloatPoint position,
Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, AsyncScrollOperationTracking operation_tracking)
{
return m_thread_data->enqueue_async_scroll_by(m_context_id, expected_document_id, position, delta_in_device_pixels, viewport_rect, operation_tracking);
}
bool CompositorThread::Context::should_defer_async_scroll_offset_adoption() const
{
return m_thread_data->should_defer_async_scroll_offset_adoption(m_context_id);
}
bool CompositorThread::Context::should_defer_main_thread_present_for_async_scroll() const
{
return m_thread_data->should_defer_main_thread_present_for_async_scroll(m_context_id);
}
CompositorThread::PendingAsyncScrollUpdates CompositorThread::Context::take_pending_async_scroll_updates()
{
return m_thread_data->take_pending_async_scroll_updates(m_context_id);
}
void CompositorThread::Context::update_scroll_state(Painting::ScrollStateSnapshot&& scroll_state_snapshot)
{
m_thread_data->enqueue_command(m_context_id, UpdateScrollStateCommand { move(scroll_state_snapshot) });
}
void CompositorThread::Context::viewport_size_updated(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress window_resize_in_progress)
{
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::Context::enqueue_viewport_size_updated(
Gfx::IntSize viewport_size, bool is_top_level_traversable, WindowResizingInProgress window_resize_in_progress)
{
m_thread_data->enqueue_command(m_context_id,
ViewportSizeUpdatedCommand { viewport_size, is_top_level_traversable, window_resize_in_progress });
}
u64 CompositorThread::Context::present_frame(Gfx::IntRect viewport_rect)
{
return m_thread_data->set_needs_present(m_context_id, viewport_rect);
}
void CompositorThread::Context::wait_for_frame(u64 frame_id)
{
m_thread_data->wait_for_frame(m_context_id, frame_id);
}
void CompositorThread::Context::request_screenshot(NonnullRefPtr<Gfx::PaintingSurface> target_surface, Function<void()>&& callback)
{
m_thread_data->enqueue_command(m_context_id, ScreenshotCommand { move(target_surface), move(callback) });
}
}