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.
This commit is contained in:
Aliaksandr Kalenik 2026-06-18 23:12:08 +02:00 committed by Alexander Kalenik
parent cb88229c3f
commit a08734ecbc
13 changed files with 114 additions and 33 deletions

View file

@ -9,7 +9,11 @@
#include <LibGfx/SharedImageBuffer.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkCanvas.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
#include <core/SkPaint.h>
#include <core/SkRect.h>
#include <core/SkSurface.h>
#include <gpu/ganesh/GrBackendSurface.h>
#include <gpu/ganesh/GrDirectContext.h>
@ -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;

View file

@ -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();

View file

@ -23,7 +23,7 @@ public:
virtual bool create_context(Gfx::IntSize, bool alpha) = 0;
virtual Optional<Painting::CanvasId> 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<Gfx::Bitmap> read_back_pixels(Gfx::IntRect const&) = 0;
};

View file

@ -241,7 +241,7 @@ WebIDL::ExceptionOr<void> 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<Gfx::Bitmap> 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<Painting::CanvasId> 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;

View file

@ -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<float> 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<RemoteCanvas2DTransport> 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 };

View file

@ -45,11 +45,27 @@ OwnPtr<Gfx::CanvasCommandPlayer> 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<Canvas2DContext>();
VERIFY(player);
return **player;
destination.copy_from_surface(source);
}
static NonnullRefPtr<Gfx::PaintingSurface> 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<Canvas2DContext>();
VERIFY(canvas_context);
return *canvas_context;
}
HostWebGLContext& CanvasHost::as_webgl(Context& context)
@ -61,12 +77,17 @@ HostWebGLContext& CanvasHost::as_webgl(Context& context)
Optional<Web::Painting::CanvasId> 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<Gfx::DecodedImageFrame> 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);

View file

@ -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<Gfx::DecodedImageFrame> const&);
ErrorOr<ByteBuffer> 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<Gfx::CanvasCommandPlayer>;
struct Canvas2DContext {
NonnullOwnPtr<Gfx::CanvasCommandPlayer> command_player;
NonnullRefPtr<Gfx::PaintingSurface> presented_surface;
bool has_uncommitted_commands { false };
};
using WebGLContext = NonnullOwnPtr<HostWebGLContext>;
using Context = Variant<Canvas2DContext, WebGLContext>;
Context* context(Web::Painting::CanvasId);
OwnPtr<Gfx::CanvasCommandPlayer> 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<Gfx::SkiaBackendContext> m_skia_backend_context;
Web::Painting::CanvasSurfaceRegistry& m_canvas_surface_registry;

View file

@ -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)

View file

@ -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)

View file

@ -46,7 +46,7 @@ private:
virtual void update_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId, NonnullRefPtr<Media::VideoFrame const>) 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;

View file

@ -97,12 +97,12 @@ Optional<Web::Painting::CanvasId> 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();
}

View file

@ -46,7 +46,7 @@ public:
void update_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId, NonnullRefPtr<Media::VideoFrame const> const&);
void clear_video_frame(Web::Compositor::CompositorContextId, Web::Painting::VideoFrameResourceId);
Optional<Web::Painting::CanvasId> 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);

View file

@ -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<Gfx::Bitmap> read_back_pixels(Gfx::IntRect const& rect) override