/* * Copyright (c) 2025-2026, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include namespace Web::HTML { struct BackingStoreState { RefPtr front_store; RefPtr back_store; i32 front_bitmap_id { -1 }; i32 back_bitmap_id { -1 }; void swap() { AK::swap(front_store, back_store); AK::swap(front_bitmap_id, back_bitmap_id); } bool is_valid() const { return front_store && back_store; } }; struct UpdateDisplayListCommand { NonnullRefPtr display_list; Painting::ScrollStateSnapshotByDisplayList scroll_state_snapshot; }; struct UpdateBackingStoresCommand { RefPtr front_store; RefPtr back_store; i32 front_bitmap_id; i32 back_bitmap_id; }; struct PresentFrameCommand { Function callback; }; struct ScreenshotCommand { NonnullRefPtr target_surface; Function callback; }; using CompositorCommand = Variant; class RenderingThread::ThreadData final : public AtomicRefCounted { public: ThreadData(Core::EventLoop& main_thread_event_loop) : m_main_thread_event_loop(main_thread_event_loop) { } ~ThreadData() = default; void set_skia_player(OwnPtr&& player) { m_skia_player = move(player); } bool has_skia_player() const { return m_skia_player != nullptr; } void exit() { Threading::MutexLocker const locker { m_mutex }; m_exit = true; m_command_ready.signal(); } void enqueue_command(CompositorCommand&& command) { Threading::MutexLocker const locker { m_mutex }; m_command_queue.enqueue(move(command)); m_command_ready.signal(); } void compositor_loop() { while (true) { auto command = [this]() -> Optional { Threading::MutexLocker const locker { m_mutex }; while (m_command_queue.is_empty() && !m_exit) { m_command_ready.wait(); } if (m_exit) return {}; return m_command_queue.dequeue(); }(); if (!command.has_value()) { VERIFY(m_exit); 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; 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(); if (cmd.callback) { invoke_on_main_thread([callback = move(cmd.callback), rendered_bitmap_id]() mutable { callback(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; } } private: template void invoke_on_main_thread(Invokee invokee) { if (m_exit) return; m_main_thread_event_loop.deferred_invoke([self = NonnullRefPtr(*this), invokee = move(invokee)]() mutable { invokee(); }); } Core::EventLoop& m_main_thread_event_loop; mutable Threading::Mutex m_mutex; mutable Threading::ConditionVariable m_command_ready { m_mutex }; Atomic m_exit { false }; Queue m_command_queue; OwnPtr m_skia_player; RefPtr m_cached_display_list; Painting::ScrollStateSnapshotByDisplayList m_cached_scroll_state_snapshot; BackingStoreState m_backing_stores; }; RenderingThread::RenderingThread() : m_thread_data(adopt_ref(*new ThreadData(Core::EventLoop::current()))) , m_main_thread_exit_promise(Core::Promise>::construct()) { // FIXME: Come up with a better "event loop exited" notification mechanism. m_main_thread_exit_promise->on_rejection = [thread_data = m_thread_data](Error const&) -> void { thread_data->exit(); }; Core::EventLoop::current().add_job(m_main_thread_exit_promise); } RenderingThread::~RenderingThread() { // Note: Promise rejection is expected to signal the thread to exit. m_main_thread_exit_promise->reject(Error::from_errno(ECANCELED)); if (m_thread) { (void)m_thread->join(); } } void RenderingThread::start(DisplayListPlayerType) { VERIFY(m_thread_data->has_skia_player()); m_thread = Threading::Thread::construct([thread_data = m_thread_data] { thread_data->compositor_loop(); return static_cast(0); }); m_thread->start(); } void RenderingThread::set_skia_player(OwnPtr&& player) { m_thread_data->set_skia_player(move(player)); } void RenderingThread::update_display_list(NonnullRefPtr display_list, Painting::ScrollStateSnapshotByDisplayList&& scroll_state_snapshot) { m_thread_data->enqueue_command(UpdateDisplayListCommand { move(display_list), move(scroll_state_snapshot) }); } void RenderingThread::update_backing_stores(RefPtr front, RefPtr back, i32 front_id, i32 back_id) { m_thread_data->enqueue_command(UpdateBackingStoresCommand { move(front), move(back), front_id, back_id }); } void RenderingThread::present_frame(Function&& callback) { m_thread_data->enqueue_command(PresentFrameCommand { move(callback) }); } void RenderingThread::request_screenshot(NonnullRefPtr target_surface, Function&& callback) { m_thread_data->enqueue_command(ScreenshotCommand { move(target_surface), move(callback) }); } }