LibGfx+LibWeb+Compositor: Remove sync wait on GPU flush

The compositor present path submitted Skia work with a synchronous GPU
wait, so each present_frame call stayed blocked until the backend
finished the submitted work. That serialized compositor processing
behind GPU completion even though the presented-bitmap acknowledgement
already controls backing-store reuse.

Add an async Skia flush-and-submit entrypoint and have the compositor
track pending presents until the Skia finished callback runs back on the
compositor event loop. did_present_frame now fires from that completion
path, while the existing presented_bitmap_ready_to_paint acknowledgement
remains the reuse gate for client-presented bitmaps.

Ganesh does not reliably run the finished proc without explicit async
work polling on this backend, so pending presents keep a completion
timer alive to call checkAsyncWorkCompletion().
This commit is contained in:
Aliaksandr Kalenik 2026-05-26 20:12:47 +02:00 committed by Alexander Kalenik
parent d6b6efa27a
commit 8950da276d
7 changed files with 213 additions and 27 deletions

View file

@ -39,10 +39,47 @@ static constexpr auto skia_resource_cache_critical_watermark = 512 * MiB;
static RefPtr<SkiaBackendContext> s_main_thread_context;
#if defined(AK_OS_MACOS) || USE_VULKAN
static void invoke_async_flush_callback(void* context)
{
auto* callback = static_cast<Function<void()>*>(context);
auto callback_to_invoke = move(*callback);
delete callback;
callback_to_invoke();
}
static void flush_and_submit_async_to_context(GrDirectContext& context, SkSurface* surface, Function<void()>&& callback)
{
GrFlushInfo flush_info {};
flush_info.fFinishedProc = invoke_async_flush_callback;
flush_info.fFinishedContext = new Function<void()>(move(callback));
context.flush(surface, SkSurfaces::BackendSurfaceAccess::kPresent, flush_info);
VERIFY(context.submit(GrSyncCpu::kNo));
}
#endif
void SkiaBackendContext::check_async_work_completion()
{
if (auto* context = sk_context())
context->checkAsyncWorkCompletion();
}
void SkiaBackendContext::flush_and_submit(SkSurface* surface)
{
flush_and_submit_impl(surface);
perform_post_flush_cleanup();
}
void SkiaBackendContext::flush_and_submit_async(SkSurface* surface, Function<void()>&& callback)
{
flush_and_submit_async_impl(surface, move(callback));
perform_post_flush_cleanup();
}
void SkiaBackendContext::perform_post_flush_cleanup()
{
auto* context = sk_context();
if (!context)
return;
@ -136,6 +173,11 @@ public:
m_context->submit(GrSyncCpu::kYes);
}
void flush_and_submit_async_impl(SkSurface* surface, Function<void()>&& callback) override
{
flush_and_submit_async_to_context(*m_context, surface, move(callback));
}
skgpu::VulkanExtensions const* extensions() const { return m_extensions.ptr(); }
GrDirectContext* sk_context() const override { return m_context.get(); }
@ -201,6 +243,11 @@ public:
m_context->submit(GrSyncCpu::kYes);
}
void flush_and_submit_async_impl(SkSurface* surface, Function<void()>&& callback) override
{
flush_and_submit_async_to_context(*m_context, surface, move(callback));
}
GrDirectContext* sk_context() const override { return m_context.get(); }
VulkanContext const& vulkan_context() override { VERIFY_NOT_REACHED(); }

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/AtomicRefCounted.h>
#include <AK/Function.h>
#include <AK/Noncopyable.h>
#ifdef USE_VULKAN
@ -46,6 +47,8 @@ public:
virtual ~SkiaBackendContext() { }
void flush_and_submit(SkSurface*);
void flush_and_submit_async(SkSurface*, Function<void()>&&);
void check_async_work_completion();
virtual GrDirectContext* sk_context() const = 0;
virtual MetalContext& metal_context() = 0;
@ -53,6 +56,10 @@ public:
protected:
virtual void flush_and_submit_impl(SkSurface*) = 0;
virtual void flush_and_submit_async_impl(SkSurface*, Function<void()>&&) = 0;
private:
void perform_post_flush_cleanup();
};
}

View file

@ -112,6 +112,16 @@ void DisplayListPlayerSkia::flush(Gfx::PaintingSurface& surface)
m_image_cache.prune();
}
void DisplayListPlayerSkia::flush_async(Gfx::PaintingSurface& surface, Function<void()>&& callback)
{
if (auto context = surface.skia_backend_context())
context->flush_and_submit_async(&surface.sk_surface(), move(callback));
else
callback();
surface.flush();
m_image_cache.prune();
}
void DisplayListPlayerSkia::draw_glyph_run(DrawGlyphRun const& command)
{
auto const& font = resource_storage().font(command.font_id);

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/Function.h>
#include <LibGfx/DecodedImageFrameSkiaImageCache.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListCommand.h>
@ -23,6 +24,7 @@ public:
~DisplayListPlayerSkia();
void flush(Gfx::PaintingSurface&) override;
void flush_async(Gfx::PaintingSurface&, Function<void()>&&);
private:
void draw_glyph_run(DrawGlyphRun const&) override;

View file

@ -21,7 +21,7 @@ target_include_directories(compositorservice PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
target_include_directories(compositorservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/)
target_link_libraries(Compositor PRIVATE compositorservice LibCore LibMain LibWebView)
target_link_libraries(compositorservice PRIVATE LibCore LibGfx LibIPC LibMedia LibWeb)
target_link_libraries(compositorservice PRIVATE LibCore LibGfx LibIPC LibMedia LibSync LibWeb)
if (WIN32)
target_include_directories(Compositor PRIVATE $<BUILD_INTERFACE:${PTHREAD_INCLUDE_DIR}>)

View file

@ -6,6 +6,7 @@
#include <AK/StdLibExtras.h>
#include <Compositor/CompositorState.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Color.h>
@ -16,6 +17,8 @@
namespace Compositor {
static constexpr int gpu_completion_check_interval_ms = 1;
static void set_or_append_pending_scroll_offset(Vector<Web::Compositor::AsyncScrollOffset>& pending_scroll_offsets, Web::Compositor::AsyncScrollOffset const& scroll_offset)
{
for (auto& existing : pending_scroll_offsets) {
@ -151,6 +154,14 @@ CompositorState::CompositorState(RefPtr<Gfx::SkiaBackendContext> skia_backend_co
{
}
CompositorState::~CompositorState()
{
if (!m_gpu_completion_timer)
return;
m_gpu_completion_timer->on_timeout = {};
m_gpu_completion_timer->stop();
}
CompositorState::ContextState::~ContextState()
{
stop_backing_store_shrink_timer();
@ -220,6 +231,7 @@ void CompositorState::destroy_context(Web::Compositor::CompositorContextId conte
auto* context = context_if_present(context_id);
VERIFY(context);
cancel_pending_async_presents_for_context(context_id);
detach_from_parent_surface(context_id, *context);
for (auto& child_context_entry : context->child_contexts_by_surface_id) {
auto* child_context = context_if_present(child_context_entry.value);
@ -258,6 +270,9 @@ void CompositorState::stop_presenting_to_client(Web::Compositor::CompositorConte
{
auto* context = context_if_present(context_id);
VERIFY(context);
if (context->gpu_present_bitmap_id_awaiting_completion.has_value()
&& context->presented_bitmap_id_awaiting_ack == context->gpu_present_bitmap_id_awaiting_completion)
context->presented_bitmap_id_awaiting_ack.clear();
context->presents_to_client = false;
}
@ -506,6 +521,7 @@ bool CompositorState::should_defer_main_thread_present_for_async_scroll(Web::Com
return context->has_deferred_async_scroll_present
|| context->pending_present_frame.has_value()
|| context->gpu_present_bitmap_id_awaiting_completion.has_value()
|| context->presented_bitmap_id_awaiting_ack.has_value();
}
@ -543,7 +559,7 @@ void CompositorState::present_frame(Web::Compositor::CompositorContextId context
void CompositorState::present_frame(Web::Compositor::CompositorContextId context_id, ContextState& context, Gfx::IntRect viewport_rect)
{
if (context.presented_bitmap_id_awaiting_ack.has_value()) {
if (context.gpu_present_bitmap_id_awaiting_completion.has_value() || context.presented_bitmap_id_awaiting_ack.has_value()) {
context.pending_present_frame = viewport_rect;
return;
}
@ -562,20 +578,37 @@ void CompositorState::present_frame(Web::Compositor::CompositorContextId context
});
m_display_list_player->execute(*context.display_list, context.display_list_resource_storage, context.scroll_state_snapshot, back_store);
paint_viewport_scrollbar_overlay(context, back_store);
m_display_list_player->flush(back_store);
auto rendered_bitmap_id = context.backing_store_manager.back_bitmap_id();
context.backing_store_manager.swap();
context.gpu_present_bitmap_id_awaiting_completion = rendered_bitmap_id;
if (context.presents_to_client)
context.presented_bitmap_id_awaiting_ack = rendered_bitmap_id;
m_pending_async_presents.append(context_id, viewport_rect, rendered_bitmap_id);
auto* pending_present = &m_pending_async_presents.last();
context.presentation_mode.visit(
[&](Empty const&) {
if (present_frame_to_client(context_id, context, viewport_rect, rendered_bitmap_id))
context.presented_bitmap_id_awaiting_ack = rendered_bitmap_id;
context.presented_frame = viewport_rect;
},
[&](Web::Compositor::PublishToCompositorSurface const& mode) {
publish_to_parent_surface(context, mode);
context.presented_frame = viewport_rect;
auto event_loop_reference = Core::EventLoop::current_weak();
auto self = NonnullRefPtr { *this };
m_display_list_player->flush_async(back_store, [self = move(self), event_loop_reference = move(event_loop_reference), pending_present] {
auto event_loop = event_loop_reference->take();
if (!event_loop.is_alive())
return;
event_loop->deferred_invoke([self = move(self), pending_present] {
self->did_finish_async_present(*pending_present);
});
});
context.backing_store_manager.swap();
context.presented_frame = viewport_rect;
schedule_gpu_completion_check();
}
void CompositorState::drain_pending_present_frame_if_unblocked(Web::Compositor::CompositorContextId context_id, ContextState& context)
{
if (context.gpu_present_bitmap_id_awaiting_completion.has_value() || context.presented_bitmap_id_awaiting_ack.has_value())
return;
if (!context.pending_present_frame.has_value())
return;
auto pending_present_frame = context.pending_present_frame.release_value();
present_frame(context_id, context, pending_present_frame);
}
bool CompositorState::request_screenshot(Web::Compositor::CompositorContextId context_id, Gfx::ShareableBitmap& target_bitmap)
@ -603,10 +636,84 @@ void CompositorState::presented_bitmap_ready_to_paint(Web::Compositor::Composito
return;
context->presented_bitmap_id_awaiting_ack.clear();
if (context->pending_present_frame.has_value()) {
auto pending_present_frame = context->pending_present_frame.release_value();
present_frame(context_id, *context, pending_present_frame);
drain_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);
VERIFY(context->gpu_present_bitmap_id_awaiting_completion == bitmap_id);
context->gpu_present_bitmap_id_awaiting_completion.clear();
if (context->presents_to_client) {
VERIFY(m_client);
VERIFY(context->presented_bitmap_id_awaiting_ack == bitmap_id);
m_client->did_present_frame(context_id, viewport_rect, bitmap_id);
} else {
context->presentation_mode.visit(
[](Empty const&) {},
[&](Web::Compositor::PublishToCompositorSurface const& mode) {
publish_to_parent_surface(*context, mode);
});
}
drain_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 || m_pending_async_presents.is_empty())
return;
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();
}
CompositorState::ContextState* CompositorState::context_if_present(Web::Compositor::CompositorContextId context_id)
@ -955,14 +1062,4 @@ void CompositorState::publish_backing_stores(Web::Compositor::CompositorContextI
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));
}
bool CompositorState::present_frame_to_client(Web::Compositor::CompositorContextId context_id, ContextState& context, Gfx::IntRect const& viewport_rect, i32 bitmap_id)
{
VERIFY(m_client);
if (!context.presents_to_client)
return false;
m_client->did_present_frame(context_id, viewport_rect, bitmap_id);
return true;
}
}

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/DoublyLinkedList.h>
#include <AK/HashMap.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
@ -58,6 +59,7 @@ public:
class CompositorState final : public RefCounted<CompositorState> {
public:
static NonnullRefPtr<CompositorState> create(RefPtr<Gfx::SkiaBackendContext>, bool async_scrolling_enabled);
~CompositorState();
enum class ContextOwnerCheckResult {
OwnedByClient,
@ -147,6 +149,7 @@ private:
Optional<Gfx::IntRect> pending_present_frame;
Optional<Gfx::IntRect> presented_frame;
Optional<i32> gpu_present_bitmap_id_awaiting_completion;
Optional<i32> presented_bitmap_id_awaiting_ack;
bool has_deferred_async_scroll_present { false };
Gfx::IntRect deferred_async_scroll_present_viewport_rect;
@ -154,6 +157,20 @@ private:
void stop_backing_store_shrink_timer();
};
struct PendingAsyncPresent {
PendingAsyncPresent(Web::Compositor::CompositorContextId context_id, Gfx::IntRect viewport_rect, i32 bitmap_id)
: context_id(context_id)
, viewport_rect(viewport_rect)
, bitmap_id(bitmap_id)
{
}
Web::Compositor::CompositorContextId context_id;
Gfx::IntRect viewport_rect;
i32 bitmap_id { 0 };
bool was_cancelled { false };
};
ContextState* context_if_present(Web::Compositor::CompositorContextId);
ContextState const* context_if_present(Web::Compositor::CompositorContextId) const;
void detach_from_parent_surface(Web::Compositor::CompositorContextId, ContextState&);
@ -177,11 +194,17 @@ private:
void publish_to_parent_surface(ContextState&, Web::Compositor::PublishToCompositorSurface const&);
void present_frame(Web::Compositor::CompositorContextId, ContextState&, Gfx::IntRect);
void publish_backing_stores(Web::Compositor::CompositorContextId, ContextState&, BackingStoreManager::Publication&&);
bool present_frame_to_client(Web::Compositor::CompositorContextId, ContextState&, Gfx::IntRect const&, i32 bitmap_id);
void did_finish_async_present(PendingAsyncPresent&);
void drain_pending_present_frame_if_unblocked(Web::Compositor::CompositorContextId, ContextState&);
void cancel_pending_async_presents_for_context(Web::Compositor::CompositorContextId);
void schedule_gpu_completion_check();
void check_gpu_completions();
HashMap<Web::Compositor::CompositorContextId, OwnPtr<ContextState>> m_contexts;
DoublyLinkedList<PendingAsyncPresent> m_pending_async_presents;
RefPtr<Gfx::SkiaBackendContext> m_skia_backend_context;
OwnPtr<Web::Painting::DisplayListPlayerSkia> m_display_list_player;
RefPtr<Core::Timer> m_gpu_completion_timer;
CompositorStateClient* m_client { nullptr };
bool m_async_scrolling_enabled { true };
};