From 116668651bdf9e58dade383dfca65d5b3e04262d Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Tue, 16 Jun 2026 06:02:08 +0200 Subject: [PATCH] LibWeb+Compositor: Avoid canvas readback for canvas drawImage 2D canvas rendering now lives in the compositor, but drawing one canvas into another still converted the source HTMLCanvasElement into a DecodedImageFrame in WebContent. That forced a compositor readback for every drawImage(canvas, ...) call before sending the destination canvas commands back to the compositor. Teach the canvas command stream to carry a DrawCanvas command that names the source canvas surface. The destination 2D context now flushes the source canvas, records that command, and immediately flushes the destination command list so the compositor copies the source surface at the drawImage call boundary. Bitmap sources continue to use DrawBitmap, and true readback APIs still read pixels explicitly. --- Libraries/LibGfx/CanvasCommandList.cpp | 27 +++++++++++++++ Libraries/LibGfx/CanvasCommandList.h | 16 +++++++++ Libraries/LibGfx/CanvasCommandPlayer.cpp | 34 ++++++++++++++++++- Libraries/LibGfx/CanvasCommandPlayer.h | 7 +++- .../LibWeb/HTML/CanvasRenderingContext2D.cpp | 33 +++++++++++++++--- Services/Compositor/CanvasHost.cpp | 4 ++- 6 files changed, 114 insertions(+), 7 deletions(-) diff --git a/Libraries/LibGfx/CanvasCommandList.cpp b/Libraries/LibGfx/CanvasCommandList.cpp index 03a0199784..bdbdb329db 100644 --- a/Libraries/LibGfx/CanvasCommandList.cpp +++ b/Libraries/LibGfx/CanvasCommandList.cpp @@ -207,6 +207,33 @@ ErrorOr decode(Decoder& decoder) }; } +template<> +ErrorOr encode(Encoder& encoder, Gfx::CanvasCommands::DrawCanvas const& command) +{ + TRY(encoder.encode(command.source_canvas_id)); + TRY(encoder.encode(command.dst_rect)); + TRY(encoder.encode(command.src_rect)); + TRY(encoder.encode(command.scaling_mode)); + TRY(encoder.encode(command.filter)); + TRY(encoder.encode(command.global_alpha)); + TRY(encoder.encode(command.compositing_and_blending_operator)); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + return Gfx::CanvasCommands::DrawCanvas { + .source_canvas_id = TRY(decoder.decode()), + .dst_rect = TRY(decoder.decode()), + .src_rect = TRY(decoder.decode()), + .scaling_mode = TRY(decoder.decode()), + .filter = TRY(decoder.decode>()), + .global_alpha = TRY(decoder.decode()), + .compositing_and_blending_operator = TRY(decoder.decode()), + }; +} + template<> ErrorOr encode(Encoder& encoder, Gfx::CanvasCommands::FillPath const& command) { diff --git a/Libraries/LibGfx/CanvasCommandList.h b/Libraries/LibGfx/CanvasCommandList.h index 85518e0a45..68bf875a19 100644 --- a/Libraries/LibGfx/CanvasCommandList.h +++ b/Libraries/LibGfx/CanvasCommandList.h @@ -82,6 +82,16 @@ struct DrawBitmap { CompositingAndBlendingOperator compositing_and_blending_operator { CompositingAndBlendingOperator::SourceOver }; }; +struct DrawCanvas { + u64 source_canvas_id { 0 }; + FloatRect dst_rect; + IntRect src_rect; + ScalingMode scaling_mode { ScalingMode::NearestNeighbor }; + Optional filter; + float global_alpha { 1 }; + CompositingAndBlendingOperator compositing_and_blending_operator { CompositingAndBlendingOperator::SourceOver }; +}; + struct FillPath { Path path; CanvasPaintStyle style; @@ -128,6 +138,7 @@ using CanvasCommand = Variant< CanvasCommands::ClearRect, CanvasCommands::FillRect, CanvasCommands::DrawBitmap, + CanvasCommands::DrawCanvas, CanvasCommands::FillPath, CanvasCommands::StrokePath, CanvasCommands::SetTransform, @@ -196,6 +207,11 @@ ErrorOr encode(Encoder&, Gfx::CanvasCommands::DrawBitmap const&); template<> ErrorOr decode(Decoder&); +template<> +ErrorOr encode(Encoder&, Gfx::CanvasCommands::DrawCanvas const&); +template<> +ErrorOr decode(Decoder&); + template<> ErrorOr encode(Encoder&, Gfx::CanvasCommands::FillPath const&); template<> diff --git a/Libraries/LibGfx/CanvasCommandPlayer.cpp b/Libraries/LibGfx/CanvasCommandPlayer.cpp index e75da2a8f7..e980fbf636 100644 --- a/Libraries/LibGfx/CanvasCommandPlayer.cpp +++ b/Libraries/LibGfx/CanvasCommandPlayer.cpp @@ -10,12 +10,16 @@ #include #include #include +#include +#include +#include namespace Gfx { -CanvasCommandPlayer::CanvasCommandPlayer(RefPtr skia_backend_context, IntSize size, BitmapFormat format, AlphaType alpha_type) +CanvasCommandPlayer::CanvasCommandPlayer(RefPtr skia_backend_context, IntSize size, BitmapFormat format, AlphaType alpha_type, CanvasSurfaceResolver canvas_surface_resolver) : m_surface(PaintingSurface::create_with_size(size, format, alpha_type, move(skia_backend_context))) , m_painter(make(*m_surface)) + , m_canvas_surface_resolver(move(canvas_surface_resolver)) { } @@ -52,6 +56,34 @@ void CanvasCommandPlayer::play_command(CanvasCommands::DrawBitmap const& command m_painter->draw_bitmap(command.dst_rect, command.frame, command.src_rect, command.scaling_mode, command.filter, command.global_alpha, command.compositing_and_blending_operator); } +void CanvasCommandPlayer::play_command(CanvasCommands::DrawCanvas const& command) +{ + if (!m_canvas_surface_resolver) + return; + + auto* source_surface = m_canvas_surface_resolver(command.source_canvas_id); + if (!source_surface) + return; + + auto image = source_surface->sk_image_snapshot>(); + if (!image) + return; + + SkPaint paint; + if (command.filter.has_value()) + paint.setImageFilter(to_skia_image_filter(command.filter.value())); + paint.setAlpha(static_cast(command.global_alpha * 255)); + paint.setBlender(to_skia_blender(command.compositing_and_blending_operator)); + + m_surface->canvas().drawImageRect( + image.get(), + to_skia_rect(command.src_rect), + to_skia_rect(command.dst_rect), + to_skia_sampling_options(command.scaling_mode), + &paint, + SkCanvas::kStrict_SrcRectConstraint); +} + void CanvasCommandPlayer::play_command(CanvasCommands::FillPath const& command) { // Shadows are recorded as blurred solid-color fills; everything else goes through the general paint-style overload. diff --git a/Libraries/LibGfx/CanvasCommandPlayer.h b/Libraries/LibGfx/CanvasCommandPlayer.h index a5d5c2aa24..eaee0c1b6e 100644 --- a/Libraries/LibGfx/CanvasCommandPlayer.h +++ b/Libraries/LibGfx/CanvasCommandPlayer.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -20,7 +21,9 @@ class CanvasCommandPlayer { AK_MAKE_NONMOVABLE(CanvasCommandPlayer); public: - CanvasCommandPlayer(RefPtr, IntSize, BitmapFormat, AlphaType); + using CanvasSurfaceResolver = Function; + + CanvasCommandPlayer(RefPtr, IntSize, BitmapFormat, AlphaType, CanvasSurfaceResolver = {}); ~CanvasCommandPlayer(); NonnullRefPtr surface() const; @@ -33,6 +36,7 @@ private: void play_command(CanvasCommands::ClearRect const&); void play_command(CanvasCommands::FillRect const&); void play_command(CanvasCommands::DrawBitmap const&); + void play_command(CanvasCommands::DrawCanvas const&); void play_command(CanvasCommands::FillPath const&); void play_command(CanvasCommands::StrokePath const&); void play_command(CanvasCommands::SetTransform const&); @@ -45,6 +49,7 @@ private: NonnullRefPtr m_surface; NonnullOwnPtr m_painter; + CanvasSurfaceResolver m_canvas_surface_resolver; }; } diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index 608c53f724..489f1bf3e6 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -175,10 +175,7 @@ WebIDL::ExceptionOr CanvasRenderingContext2D::draw_image_internal(CanvasIm if (usability == CanvasImageSourceUsability::Bad) return {}; - auto frame = canvas_image_source_frame(image); - if (!frame.has_value()) - return {}; - auto source_bitmap_rect = frame->rect(); + auto source_bitmap_rect = Gfx::IntRect { {}, canvas_image_source_dimensions(image) }; // 4. Establish the source and destination rectangles as follows: // If not specified, the dw and dh arguments must default to the values of sw and sh, interpreted such that one CSS pixel in the image is treated as one unit in the output bitmap's coordinate space. @@ -230,6 +227,34 @@ WebIDL::ExceptionOr CanvasRenderingContext2D::draw_image_internal(CanvasIm } if (auto* canvas_command_list = this->canvas_command_list()) { + if (auto const* source_canvas = image.get_pointer>()) { + (*source_canvas)->ensure_backing_storage(); + (*source_canvas)->prepare_for_compositing(); + if (auto source_canvas_id = (*source_canvas)->canvas_id(); source_canvas_id.has_value()) { + canvas_command_list->append(Gfx::CanvasCommands::DrawCanvas { + .source_canvas_id = source_canvas_id->value(), + .dst_rect = destination_rect, + .src_rect = source_rect.to_rounded(), + .scaling_mode = scaling_mode, + .filter = drawing_state().filter, + .global_alpha = drawing_state().global_alpha, + .compositing_and_blending_operator = drawing_state().current_compositing_and_blending_operator, + }); + did_draw(destination_rect); + flush_recorded_commands(); + + // 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false. + if (image_is_not_origin_clean(image)) + m_origin_clean = false; + + return {}; + } + } + + auto frame = canvas_image_source_frame(image); + if (!frame.has_value()) + return {}; + canvas_command_list->append(Gfx::CanvasCommands::DrawBitmap { .frame = *frame, .dst_rect = destination_rect, diff --git a/Services/Compositor/CanvasHost.cpp b/Services/Compositor/CanvasHost.cpp index b1822733a0..d945f034d8 100644 --- a/Services/Compositor/CanvasHost.cpp +++ b/Services/Compositor/CanvasHost.cpp @@ -32,7 +32,9 @@ OwnPtr CanvasHost::create_2d_command_player(Gfx::IntSi return nullptr; auto format = alpha ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888; - auto player = make(m_skia_backend_context, size, format, Gfx::AlphaType::Premultiplied); + auto player = make(m_skia_backend_context, size, format, Gfx::AlphaType::Premultiplied, [this](u64 canvas_id) -> Gfx::PaintingSurface const* { + return m_canvas_surface_registry.canvas_surface(Web::Painting::CanvasId { canvas_id }); + }); // https://html.spec.whatwg.org/multipage/canvas.html#the-canvas-settings:concept-canvas-alpha // "Thus, the bitmap of such a context starts off as opaque black instead of transparent black"