LibWeb: Move rendering backpressure from main thread to RenderingThread

In preparation for handling input events on the rendering thread, move
backpressure management to RenderingThread. The rendering thread needs
to manage this independently without querying the main thread.

Previously, the main thread would block when the UI process hadn't yet
released the backing surface. Now, the main thread can continue
producing display lists while the UI process is busy, allowing more work
to happen in parallel. When rasterization is slow and display lists are
produced faster than they can be consumed, presentation requests are
naturally coalesced using a flag-based approach -
multiple present_frame() calls simply update the pending state,
resulting in a single rasterization with the latest display list.
This commit is contained in:
Aliaksandr Kalenik
2026-01-26 17:47:49 +01:00
committed by Alexander Kalenik
parent 1c3d503146
commit a86f318a9b
Notes: github-actions[bot] 2026-02-06 11:04:12 +00:00
4 changed files with 105 additions and 57 deletions

View File

@@ -39,16 +39,12 @@ struct UpdateBackingStoresCommand {
i32 back_bitmap_id;
};
struct PresentFrameCommand {
Gfx::IntRect viewport_rect;
};
struct ScreenshotCommand {
NonnullRefPtr<Gfx::PaintingSurface> target_surface;
Function<void()> callback;
};
using CompositorCommand = Variant<UpdateDisplayListCommand, UpdateBackingStoresCommand, PresentFrameCommand, ScreenshotCommand>;
using CompositorCommand = Variant<UpdateDisplayListCommand, UpdateBackingStoresCommand, ScreenshotCommand>;
class RenderingThread::ThreadData final : public AtomicRefCounted<ThreadData> {
public:
@@ -72,6 +68,7 @@ public:
Threading::MutexLocker const locker { m_mutex };
m_exit = true;
m_command_ready.signal();
m_ready_to_paint.signal();
}
void enqueue_command(CompositorCommand&& command)
@@ -81,60 +78,100 @@ public:
m_command_ready.signal();
}
void set_needs_present(Gfx::IntRect viewport_rect)
{
Threading::MutexLocker const locker { m_mutex };
m_needs_present = true;
m_pending_viewport_rect = viewport_rect;
m_command_ready.signal();
}
void compositor_loop()
{
while (true) {
auto command = [this]() -> Optional<CompositorCommand> {
{
Threading::MutexLocker const locker { m_mutex };
while (m_command_queue.is_empty() && !m_exit) {
while (m_command_queue.is_empty() && !m_needs_present && !m_exit) {
m_command_ready.wait();
}
if (m_exit)
return {};
return m_command_queue.dequeue();
}();
if (!command.has_value()) {
VERIFY(m_exit);
break;
break;
}
command->visit(
[this](UpdateDisplayListCommand& cmd) {
m_cached_display_list = move(cmd.display_list);
m_cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
},
[this](UpdateBackingStoresCommand& cmd) {
m_backing_stores.front_store = move(cmd.front_store);
m_backing_stores.back_store = move(cmd.back_store);
m_backing_stores.front_bitmap_id = cmd.front_bitmap_id;
m_backing_stores.back_bitmap_id = cmd.back_bitmap_id;
},
[this](PresentFrameCommand& cmd) {
if (!m_cached_display_list || !m_backing_stores.is_valid())
return;
while (true) {
auto command = [this]() -> Optional<CompositorCommand> {
Threading::MutexLocker const locker { m_mutex };
if (m_command_queue.is_empty())
return {};
return m_command_queue.dequeue();
}();
if (!command.has_value())
break;
command->visit(
[this](UpdateDisplayListCommand& cmd) {
m_cached_display_list = move(cmd.display_list);
m_cached_scroll_state_snapshot = move(cmd.scroll_state_snapshot);
},
[this](UpdateBackingStoresCommand& cmd) {
m_backing_stores.front_store = move(cmd.front_store);
m_backing_stores.back_store = move(cmd.back_store);
m_backing_stores.front_bitmap_id = cmd.front_bitmap_id;
m_backing_stores.back_bitmap_id = cmd.back_bitmap_id;
},
[this](ScreenshotCommand& cmd) {
if (!m_cached_display_list)
return;
m_skia_player->execute(*m_cached_display_list, Painting::ScrollStateSnapshotByDisplayList(m_cached_scroll_state_snapshot), *cmd.target_surface);
if (cmd.callback) {
invoke_on_main_thread([callback = move(cmd.callback)]() mutable {
callback();
});
}
});
if (m_exit)
break;
}
if (m_exit)
break;
bool should_present = false;
Gfx::IntRect viewport_rect;
{
Threading::MutexLocker const locker { m_mutex };
if (m_needs_present) {
should_present = true;
viewport_rect = m_pending_viewport_rect;
m_needs_present = false;
}
}
if (should_present) {
// Block if we already have a frame queued (back pressure)
{
Threading::MutexLocker const locker { m_mutex };
while (m_queued_rasterization_tasks > 1 && !m_exit) {
m_ready_to_paint.wait();
}
if (m_exit)
break;
}
if (m_cached_display_list && m_backing_stores.is_valid()) {
m_skia_player->execute(*m_cached_display_list, Painting::ScrollStateSnapshotByDisplayList(m_cached_scroll_state_snapshot), *m_backing_stores.back_store);
i32 rendered_bitmap_id = m_backing_stores.back_bitmap_id;
m_backing_stores.swap();
invoke_on_main_thread([this, viewport_rect = cmd.viewport_rect, rendered_bitmap_id]() {
m_queued_rasterization_tasks++;
invoke_on_main_thread([this, viewport_rect, rendered_bitmap_id]() {
m_presentation_callback(viewport_rect, rendered_bitmap_id);
});
},
[this](ScreenshotCommand& cmd) {
if (!m_cached_display_list)
return;
m_skia_player->execute(*m_cached_display_list, Painting::ScrollStateSnapshotByDisplayList(m_cached_scroll_state_snapshot), *cmd.target_surface);
if (cmd.callback) {
invoke_on_main_thread([callback = move(cmd.callback)]() mutable {
callback();
});
}
});
if (m_exit)
break;
}
}
}
}
@@ -162,6 +199,21 @@ private:
RefPtr<Painting::DisplayList> m_cached_display_list;
Painting::ScrollStateSnapshotByDisplayList m_cached_scroll_state_snapshot;
BackingStoreState m_backing_stores;
Atomic<i32> m_queued_rasterization_tasks { 0 };
mutable Threading::ConditionVariable m_ready_to_paint { m_mutex };
bool m_needs_present { false };
Gfx::IntRect m_pending_viewport_rect;
public:
void decrement_queued_tasks()
{
Threading::MutexLocker const locker { m_mutex };
VERIFY(m_queued_rasterization_tasks >= 1 && m_queued_rasterization_tasks <= 2);
m_queued_rasterization_tasks--;
m_ready_to_paint.signal();
}
};
RenderingThread::RenderingThread(PresentationCallback presentation_callback)
@@ -211,7 +263,7 @@ void RenderingThread::update_backing_stores(RefPtr<Gfx::PaintingSurface> front,
void RenderingThread::present_frame(Gfx::IntRect viewport_rect)
{
m_thread_data->enqueue_command(PresentFrameCommand { viewport_rect });
m_thread_data->set_needs_present(viewport_rect);
}
void RenderingThread::request_screenshot(NonnullRefPtr<Gfx::PaintingSurface> target_surface, Function<void()>&& callback)
@@ -219,4 +271,9 @@ void RenderingThread::request_screenshot(NonnullRefPtr<Gfx::PaintingSurface> tar
m_thread_data->enqueue_command(ScreenshotCommand { move(target_surface), move(callback) });
}
void RenderingThread::ready_to_paint()
{
m_thread_data->decrement_queued_tasks();
}
}