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.
This commit is contained in:
Aliaksandr Kalenik 2026-06-16 06:02:08 +02:00 committed by Alexander Kalenik
parent f215d9bb9a
commit 116668651b
6 changed files with 114 additions and 7 deletions

View file

@ -207,6 +207,33 @@ ErrorOr<Gfx::CanvasCommands::DrawBitmap> decode(Decoder& decoder)
};
}
template<>
ErrorOr<void> 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<Gfx::CanvasCommands::DrawCanvas> decode(Decoder& decoder)
{
return Gfx::CanvasCommands::DrawCanvas {
.source_canvas_id = TRY(decoder.decode<u64>()),
.dst_rect = TRY(decoder.decode<Gfx::FloatRect>()),
.src_rect = TRY(decoder.decode<Gfx::IntRect>()),
.scaling_mode = TRY(decoder.decode<Gfx::ScalingMode>()),
.filter = TRY(decoder.decode<Optional<Gfx::Filter>>()),
.global_alpha = TRY(decoder.decode<float>()),
.compositing_and_blending_operator = TRY(decoder.decode<Gfx::CompositingAndBlendingOperator>()),
};
}
template<>
ErrorOr<void> encode(Encoder& encoder, Gfx::CanvasCommands::FillPath const& command)
{

View file

@ -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> 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<void> encode(Encoder&, Gfx::CanvasCommands::DrawBitmap const&);
template<>
ErrorOr<Gfx::CanvasCommands::DrawBitmap> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, Gfx::CanvasCommands::DrawCanvas const&);
template<>
ErrorOr<Gfx::CanvasCommands::DrawCanvas> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, Gfx::CanvasCommands::FillPath const&);
template<>

View file

@ -10,12 +10,16 @@
#include <LibGfx/Color.h>
#include <LibGfx/PainterSkia.h>
#include <LibGfx/PaintingSurface.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkCanvas.h>
#include <core/SkPaint.h>
namespace Gfx {
CanvasCommandPlayer::CanvasCommandPlayer(RefPtr<SkiaBackendContext> skia_backend_context, IntSize size, BitmapFormat format, AlphaType alpha_type)
CanvasCommandPlayer::CanvasCommandPlayer(RefPtr<SkiaBackendContext> 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<PainterSkia>(*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<sk_sp<SkImage>>();
if (!image)
return;
SkPaint paint;
if (command.filter.has_value())
paint.setImageFilter(to_skia_image_filter(command.filter.value()));
paint.setAlpha(static_cast<u8>(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.

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/Function.h>
#include <AK/Noncopyable.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
@ -20,7 +21,9 @@ class CanvasCommandPlayer {
AK_MAKE_NONMOVABLE(CanvasCommandPlayer);
public:
CanvasCommandPlayer(RefPtr<SkiaBackendContext>, IntSize, BitmapFormat, AlphaType);
using CanvasSurfaceResolver = Function<PaintingSurface const*(u64)>;
CanvasCommandPlayer(RefPtr<SkiaBackendContext>, IntSize, BitmapFormat, AlphaType, CanvasSurfaceResolver = {});
~CanvasCommandPlayer();
NonnullRefPtr<PaintingSurface> 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<PaintingSurface> m_surface;
NonnullOwnPtr<PainterSkia> m_painter;
CanvasSurfaceResolver m_canvas_surface_resolver;
};
}

View file

@ -175,10 +175,7 @@ WebIDL::ExceptionOr<void> 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<void> CanvasRenderingContext2D::draw_image_internal(CanvasIm
}
if (auto* canvas_command_list = this->canvas_command_list()) {
if (auto const* source_canvas = image.get_pointer<GC::Ref<HTMLCanvasElement>>()) {
(*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<int>(),
.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,

View file

@ -32,7 +32,9 @@ OwnPtr<Gfx::CanvasCommandPlayer> CanvasHost::create_2d_command_player(Gfx::IntSi
return nullptr;
auto format = alpha ? Gfx::BitmapFormat::BGRA8888 : Gfx::BitmapFormat::BGRx8888;
auto player = make<Gfx::CanvasCommandPlayer>(m_skia_backend_context, size, format, Gfx::AlphaType::Premultiplied);
auto player = make<Gfx::CanvasCommandPlayer>(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"