From a08734ecbcf4ebcaa2a961844e67833ae9dc2bf0 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Thu, 18 Jun 2026 23:12:08 +0200 Subject: [PATCH] LibGfx+LibWeb+Compositor+WebContent: Commit 2D canvas flushes atomically 2D canvas contexts started publishing partial frames after canvas rasterization moved into the Compositor. WebContent still splits large recorded command lists after 64 commands, but every split batch was sent through the same compositor path as the end-of-frame flush. The compositor replayed each batch into the DrawCanvas source surface, so a pending present could sample a canvas after clear and before the rest of the next frame had been drawn. Canvas-heavy pages such as slither.com then flickered between partial and complete frames. Carry an explicit commit bit with 2D canvas command updates. Non-commit batches now update a hidden working canvas in the Compositor, while the display-list-visible surface keeps the last committed canvas contents. The end-of-frame canvas preparation sends the commit boundary, including the empty-commit case needed when the auto-flush consumed all recorded commands before prepare_for_compositing() runs. --- Libraries/LibGfx/PaintingSurface.cpp | 23 +++++++ Libraries/LibGfx/PaintingSurface.h | 1 + .../HTML/Canvas/RemoteCanvas2DTransport.h | 2 +- .../LibWeb/HTML/CanvasRenderingContext2D.cpp | 25 +++++--- .../LibWeb/HTML/CanvasRenderingContext2D.h | 7 ++- Services/Compositor/CanvasHost.cpp | 60 +++++++++++++++---- Services/Compositor/CanvasHost.h | 11 +++- .../Compositor/CompositorWebContentServer.ipc | 2 +- .../Compositor/ConnectionFromWebContent.cpp | 4 +- .../Compositor/ConnectionFromWebContent.h | 2 +- Services/WebContent/CompositorConnection.cpp | 4 +- Services/WebContent/CompositorConnection.h | 2 +- .../WebContent/WebContentCompositorHost.cpp | 4 +- 13 files changed, 114 insertions(+), 33 deletions(-) diff --git a/Libraries/LibGfx/PaintingSurface.cpp b/Libraries/LibGfx/PaintingSurface.cpp index 32ceda1cf5..220f683598 100644 --- a/Libraries/LibGfx/PaintingSurface.cpp +++ b/Libraries/LibGfx/PaintingSurface.cpp @@ -9,7 +9,11 @@ #include #include +#include #include +#include +#include +#include #include #include #include @@ -178,6 +182,25 @@ void PaintingSurface::write_from_bitmap(Bitmap const& bitmap) m_impl->surface->writePixels(pixmap, 0, 0); } +void PaintingSurface::copy_from_surface(PaintingSurface& source) +{ + source.flush(); + + auto image = source.m_impl->surface->makeImageSnapshot(); + if (!image) + return; + + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); + canvas().drawImageRect( + image.get(), + SkRect::MakeIWH(image->width(), image->height()), + SkRect::MakeIWH(size().width(), size().height()), + SkSamplingOptions {}, + &paint, + SkCanvas::kStrict_SrcRectConstraint); +} + IntSize PaintingSurface::size() const { return m_impl->size; diff --git a/Libraries/LibGfx/PaintingSurface.h b/Libraries/LibGfx/PaintingSurface.h index 1ce37d8d94..4948479cb4 100644 --- a/Libraries/LibGfx/PaintingSurface.h +++ b/Libraries/LibGfx/PaintingSurface.h @@ -58,6 +58,7 @@ public: void read_into_bitmap(Bitmap&, IntPoint source_position = {}) const; void write_from_bitmap(Bitmap const&); + void copy_from_surface(PaintingSurface&); void notify_content_will_change(); diff --git a/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h b/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h index ca68ed96ff..27ebc0969d 100644 --- a/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h +++ b/Libraries/LibWeb/HTML/Canvas/RemoteCanvas2DTransport.h @@ -23,7 +23,7 @@ public: virtual bool create_context(Gfx::IntSize, bool alpha) = 0; virtual Optional canvas_id() const = 0; virtual void destroy_context() = 0; - virtual void update_commands(Gfx::CanvasCommandList const&) = 0; + virtual void update_commands(Gfx::CanvasCommandList const&, bool commit) = 0; virtual RefPtr read_back_pixels(Gfx::IntRect const&) = 0; }; diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index 489f1bf3e6..d463fbdfe9 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -241,7 +241,7 @@ WebIDL::ExceptionOr CanvasRenderingContext2D::draw_image_internal(CanvasIm .compositing_and_blending_operator = drawing_state().current_compositing_and_blending_operator, }); did_draw(destination_rect); - flush_recorded_commands(); + flush_recorded_commands(CommitCommands::No); // 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false. if (image_is_not_origin_clean(image)) @@ -289,7 +289,7 @@ Gfx::CanvasCommandList* CanvasRenderingContext2D::canvas_command_list() if (!has_backing_storage()) return nullptr; if (m_commands.size() >= max_pending_canvas_commands) - flush_recorded_commands(); + flush_recorded_commands(CommitCommands::No); return &m_commands; } @@ -315,20 +315,30 @@ bool CanvasRenderingContext2D::ensure_remote_canvas_context() return true; } -void CanvasRenderingContext2D::flush_recorded_commands() +void CanvasRenderingContext2D::flush_recorded_commands(CommitCommands commit) { - if (m_commands.is_empty() || !m_transport) + if (!m_transport) return; + bool const should_commit = commit == CommitCommands::Yes; + if (m_commands.is_empty()) { + if (!should_commit || !m_has_uncommitted_remote_commands) + return; + m_transport->update_commands(m_commands, true); + m_has_uncommitted_remote_commands = false; + return; + } + auto commands = move(m_commands); - m_transport->update_commands(commands); + m_transport->update_commands(commands, should_commit); + m_has_uncommitted_remote_commands = !should_commit; } RefPtr CanvasRenderingContext2D::read_pixels(Gfx::IntRect const& rect) { if (!has_backing_storage()) return nullptr; - flush_recorded_commands(); + flush_recorded_commands(CommitCommands::No); return m_transport->read_back_pixels(rect); } @@ -342,7 +352,7 @@ void CanvasRenderingContext2D::set_size(Gfx::IntSize const& size) void CanvasRenderingContext2D::prepare_for_compositing() { - flush_recorded_commands(); + flush_recorded_commands(CommitCommands::Yes); } Optional CanvasRenderingContext2D::canvas_id() const @@ -413,6 +423,7 @@ void CanvasRenderingContext2D::ensure_backing_storage() void CanvasRenderingContext2D::discard_backing_storage() { m_commands = {}; + m_has_uncommitted_remote_commands = false; if (m_transport) { m_transport->destroy_context(); m_transport = nullptr; diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h index 73347d5dee..736927b0dd 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.h @@ -179,7 +179,11 @@ private: void paint_shadow_for_fill_internal(Gfx::Path const&, Gfx::WindingRule); void paint_shadow_for_stroke_internal(Gfx::Path const&, Gfx::Path::CapStyle, Gfx::Path::JoinStyle, Vector const&); - void flush_recorded_commands(); + enum class CommitCommands { + No, + Yes, + }; + void flush_recorded_commands(CommitCommands); bool ensure_remote_canvas_context(); @@ -189,6 +193,7 @@ private: Gfx::CanvasCommandList m_commands; RefPtr m_transport; + bool m_has_uncommitted_remote_commands { false }; // https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-origin-clean bool m_origin_clean { true }; diff --git a/Services/Compositor/CanvasHost.cpp b/Services/Compositor/CanvasHost.cpp index d945f034d8..8c338784ec 100644 --- a/Services/Compositor/CanvasHost.cpp +++ b/Services/Compositor/CanvasHost.cpp @@ -45,11 +45,27 @@ OwnPtr CanvasHost::create_2d_command_player(Gfx::IntSi return player; } -Gfx::CanvasCommandPlayer& CanvasHost::as_2d(Context& context) +static void copy_surface_contents(Gfx::PaintingSurface& source, Gfx::PaintingSurface& destination) { - auto* player = context.get_pointer(); - VERIFY(player); - return **player; + destination.copy_from_surface(source); +} + +static NonnullRefPtr create_presented_canvas_surface(Gfx::PaintingSurface& source) +{ + auto surface = Gfx::PaintingSurface::create_with_size( + source.size(), + Gfx::BitmapFormat::BGRA8888, + Gfx::AlphaType::Premultiplied, + source.skia_backend_context()); + copy_surface_contents(source, *surface); + return surface; +} + +CanvasHost::Canvas2DContext& CanvasHost::as_2d(Context& context) +{ + auto* canvas_context = context.get_pointer(); + VERIFY(canvas_context); + return *canvas_context; } HostWebGLContext& CanvasHost::as_webgl(Context& context) @@ -61,12 +77,17 @@ HostWebGLContext& CanvasHost::as_webgl(Context& context) Optional CanvasHost::create_2d_context(Gfx::IntSize size, bool alpha) { - auto context = create_2d_command_player(size, alpha); - if (!context) + auto command_player = create_2d_command_player(size, alpha); + if (!command_player) return {}; - auto canvas_id = m_canvas_surface_registry.create_canvas_surface(context->surface()); - m_contexts.set(canvas_id, context.release_nonnull()); + auto presented_surface = create_presented_canvas_surface(command_player->surface()); + auto canvas_id = m_canvas_surface_registry.create_canvas_surface(presented_surface); + Canvas2DContext context { + .command_player = command_player.release_nonnull(), + .presented_surface = move(presented_surface), + }; + m_contexts.set(canvas_id, move(context)); return canvas_id; } @@ -104,11 +125,26 @@ CanvasHost::Context* CanvasHost::context(Web::Painting::CanvasId canvas_id) return &it->value; } -void CanvasHost::execute_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands) +void CanvasHost::present_canvas_2d_context(Web::Painting::CanvasId canvas_id, Canvas2DContext& context) +{ + copy_surface_contents(context.command_player->surface(), context.presented_surface); + m_canvas_surface_registry.set_canvas_surface(canvas_id, context.presented_surface); + context.has_uncommitted_commands = false; +} + +void CanvasHost::execute_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands, bool commit) { auto* context = this->context(canvas_id); VERIFY(context); - as_2d(*context).play(commands); + + auto& canvas_context = as_2d(*context); + if (!commands.is_empty()) { + canvas_context.command_player->play(commands); + canvas_context.has_uncommitted_commands = true; + } + + if (commit && canvas_context.has_uncommitted_commands) + present_canvas_2d_context(canvas_id, canvas_context); } void CanvasHost::execute_webgl_commands(Web::Painting::CanvasId canvas_id, ByteBuffer const& commands, Vector const& bitmaps) @@ -174,8 +210,8 @@ Gfx::ShareableBitmap CanvasHost::read_back_pixels(Web::Painting::CanvasId canvas return {}; return context->visit( - [rect](Canvas2DContext& player) { - return read_back_surface(player->surface(), rect); + [rect](Canvas2DContext& canvas_context) { + return read_back_surface(canvas_context.command_player->surface(), rect); }, [rect](WebGLContext& webgl_context) { return webgl_context->read_back_drawing_buffer(rect); diff --git a/Services/Compositor/CanvasHost.h b/Services/Compositor/CanvasHost.h index f9a277c49c..a5f8359f4a 100644 --- a/Services/Compositor/CanvasHost.h +++ b/Services/Compositor/CanvasHost.h @@ -50,7 +50,7 @@ public: void destroy_context(Web::Painting::CanvasId); bool has_context(Web::Painting::CanvasId) const; - void execute_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&); + void execute_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&, bool commit); void execute_webgl_commands(Web::Painting::CanvasId, ByteBuffer const&, Vector const&); ErrorOr execute_webgl_sync_call(Web::Painting::CanvasId, ByteBuffer request); Web::WebGL::ReadPixelsResult webgl_read_pixels_robust_angle(Web::Painting::CanvasId, Web::WebGL::GLint x, Web::WebGL::GLint y, Web::WebGL::GLsizei width, Web::WebGL::GLsizei height, Web::WebGL::GLenum format, Web::WebGL::GLenum type, Web::WebGL::GLsizei buf_size, Core::AnonymousBuffer pixels); @@ -60,14 +60,19 @@ public: Gfx::ShareableBitmap read_back_pixels(Web::Painting::CanvasId, Gfx::IntRect); private: - using Canvas2DContext = NonnullOwnPtr; + struct Canvas2DContext { + NonnullOwnPtr command_player; + NonnullRefPtr presented_surface; + bool has_uncommitted_commands { false }; + }; using WebGLContext = NonnullOwnPtr; using Context = Variant; Context* context(Web::Painting::CanvasId); OwnPtr create_2d_command_player(Gfx::IntSize, bool alpha); - static Gfx::CanvasCommandPlayer& as_2d(Context&); + static Canvas2DContext& as_2d(Context&); static HostWebGLContext& as_webgl(Context&); + void present_canvas_2d_context(Web::Painting::CanvasId, Canvas2DContext&); RefPtr m_skia_backend_context; Web::Painting::CanvasSurfaceRegistry& m_canvas_surface_registry; diff --git a/Services/Compositor/CompositorWebContentServer.ipc b/Services/Compositor/CompositorWebContentServer.ipc index ddcb72a363..b59b952a84 100644 --- a/Services/Compositor/CompositorWebContentServer.ipc +++ b/Services/Compositor/CompositorWebContentServer.ipc @@ -32,7 +32,7 @@ endpoint CompositorWebContentServer clear_video_frame(Web::Compositor::CompositorContextId context_id, Web::Painting::VideoFrameResourceId frame_id) =| create_canvas_2d_context(Gfx::IntSize size, bool alpha) => (bool success, Web::Painting::CanvasId canvas_id) - update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands) =| + update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands, bool commit) =| destroy_canvas_context(Web::Painting::CanvasId canvas_id) =| get_canvas_pixels(Web::Painting::CanvasId canvas_id, Gfx::IntRect rect) => (Gfx::ShareableBitmap pixels) diff --git a/Services/Compositor/ConnectionFromWebContent.cpp b/Services/Compositor/ConnectionFromWebContent.cpp index 5455f504ad..dda5a4b345 100644 --- a/Services/Compositor/ConnectionFromWebContent.cpp +++ b/Services/Compositor/ConnectionFromWebContent.cpp @@ -120,9 +120,9 @@ Messages::CompositorWebContentServer::CreateCanvas2dContextResponse ConnectionFr return { true, *canvas_id }; } -void ConnectionFromWebContent::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands) +void ConnectionFromWebContent::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList commands, bool commit) { - m_canvas_host.execute_canvas_2d_commands(canvas_id, commands); + m_canvas_host.execute_canvas_2d_commands(canvas_id, commands, commit); } void ConnectionFromWebContent::destroy_canvas_context(Web::Painting::CanvasId canvas_id) diff --git a/Services/Compositor/ConnectionFromWebContent.h b/Services/Compositor/ConnectionFromWebContent.h index 6e5d2d8ca7..c4a50c2608 100644 --- a/Services/Compositor/ConnectionFromWebContent.h +++ b/Services/Compositor/ConnectionFromWebContent.h @@ -46,7 +46,7 @@ private: virtual void update_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId, NonnullRefPtr) override; virtual void clear_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId) override; virtual Messages::CompositorWebContentServer::CreateCanvas2dContextResponse create_canvas_2d_context(Gfx::IntSize, bool) override; - virtual void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList) override; + virtual void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList, bool commit) override; virtual void destroy_canvas_context(Web::Painting::CanvasId) override; virtual Messages::CompositorWebContentServer::GetCanvasPixelsResponse get_canvas_pixels(Web::Painting::CanvasId, Gfx::IntRect) override; diff --git a/Services/WebContent/CompositorConnection.cpp b/Services/WebContent/CompositorConnection.cpp index a681c16549..ee9f8993f2 100644 --- a/Services/WebContent/CompositorConnection.cpp +++ b/Services/WebContent/CompositorConnection.cpp @@ -97,12 +97,12 @@ Optional CompositorConnection::create_canvas_2d_context return response->canvas_id(); } -void CompositorConnection::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands) +void CompositorConnection::update_canvas_2d_commands(Web::Painting::CanvasId canvas_id, Gfx::CanvasCommandList const& commands, bool commit) { if (!can_send_message_to_compositor()) return; - auto encoded_message = MUST(Messages::CompositorWebContentServer::UpdateCanvas2dCommands::static_encode(canvas_id, commands)); + auto encoded_message = MUST(Messages::CompositorWebContentServer::UpdateCanvas2dCommands::static_encode(canvas_id, commands, commit)); if (post_message(encoded_message).is_error()) did_lose_compositor(); } diff --git a/Services/WebContent/CompositorConnection.h b/Services/WebContent/CompositorConnection.h index 91eef82585..89545d179f 100644 --- a/Services/WebContent/CompositorConnection.h +++ b/Services/WebContent/CompositorConnection.h @@ -46,7 +46,7 @@ public: void update_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId, NonnullRefPtr const&); void clear_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId); Optional create_canvas_2d_context(Gfx::IntSize, bool alpha); - void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&); + void update_canvas_2d_commands(Web::Painting::CanvasId, Gfx::CanvasCommandList const&, bool commit); void destroy_canvas_context(Web::Painting::CanvasId); Gfx::ShareableBitmap get_canvas_pixels(Web::Painting::CanvasId, Gfx::IntRect); void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation); diff --git a/Services/WebContent/WebContentCompositorHost.cpp b/Services/WebContent/WebContentCompositorHost.cpp index a7058f4139..301a4e7e36 100644 --- a/Services/WebContent/WebContentCompositorHost.cpp +++ b/Services/WebContent/WebContentCompositorHost.cpp @@ -128,11 +128,11 @@ private: m_canvas_id.clear(); } - virtual void update_commands(Gfx::CanvasCommandList const& commands) override + virtual void update_commands(Gfx::CanvasCommandList const& commands, bool commit) override { if (!m_canvas_id.has_value()) return; - m_connection->update_canvas_2d_commands(*m_canvas_id, commands); + m_connection->update_canvas_2d_commands(*m_canvas_id, commands, commit); } virtual RefPtr read_back_pixels(Gfx::IntRect const& rect) override