LibWeb: Let image paint callers own clipping

DecodedImageData::paint() used to take both a destination and a
clip rectangle even though most callers passed the same value. SVG
image painting used that API to wrap every nested SVG display list in
save/add-clip/restore, which put an unbounded command in front of
the bounded nested-list command and made offscreen SVG image content
harder to cull.

Move clipping to ImagePaintable, where the object-fit destination can
be compared with the replaced element box. CSS image and marker
painting continue to draw into their destination rect, while repeated
background images keep their explicit tile clip. The scaled decoded
image display-list command now stores only its destination rect and
uses that as its bounds; playback still clips decoded images to that
rect so bitmap rendering stays unchanged.
This commit is contained in:
Aliaksandr Kalenik 2026-06-06 13:36:58 +02:00 committed by Alexander Kalenik
parent cdb8762c8f
commit 852c7a10f3
19 changed files with 88 additions and 46 deletions

View file

@ -244,7 +244,7 @@ void ImageStyleValue::paint(DisplayListRecordingContext& context, DevicePixelRec
auto dest_int_rect = dest_rect.to_type<int>();
auto rect = image_data->frame_rect(m_current_frame_index).value_or(dest_int_rect);
auto scaling_mode = to_gfx_scaling_mode(image_rendering, rect.size(), dest_int_rect.size());
image_data->paint(context, m_current_frame_index, dest_int_rect, dest_int_rect, scaling_mode);
image_data->paint(context, m_current_frame_index, dest_int_rect, scaling_mode);
}
Optional<Gfx::DecodedImageFrame> ImageStyleValue::current_frame(DevicePixelRect const& dest_rect) const

View file

@ -179,12 +179,12 @@ Optional<Gfx::IntRect> AnimatedDecodedImageData::frame_rect(size_t) const
return Gfx::IntRect { {}, m_size };
}
void AnimatedDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const
void AnimatedDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::ScalingMode scaling_mode) const
{
auto decoded_frame = frame(frame_index);
if (!decoded_frame.has_value())
return;
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, clip_rect, *decoded_frame, scaling_mode);
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, *decoded_frame, scaling_mode);
}
void AnimatedDecodedImageData::receive_frames(Vector<NonnullRefPtr<Gfx::Bitmap>> bitmaps, u32 start_frame_index)

View file

@ -48,7 +48,7 @@ public:
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
virtual Optional<Gfx::IntRect> frame_rect(size_t frame_index) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::ScalingMode) const override;
virtual size_t notify_frame_advanced(size_t caller_frame_index) override;

View file

@ -72,9 +72,9 @@ Optional<Gfx::IntRect> BitmapDecodedImageData::frame_rect(size_t frame_index) co
return m_frames[frame_index].frame.rect();
}
void BitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const
void BitmapDecodedImageData::paint(DisplayListRecordingContext& context, size_t frame_index, Gfx::IntRect dst_rect, Gfx::ScalingMode scaling_mode) const
{
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, clip_rect, m_frames[frame_index].frame, scaling_mode);
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, m_frames[frame_index].frame, scaling_mode);
}
}

View file

@ -37,7 +37,7 @@ public:
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
virtual Optional<Gfx::IntRect> frame_rect(size_t frame_index) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::ScalingMode scaling_mode) const override;
private:
BitmapDecodedImageData(Vector<Frame>&&, size_t loop_count, bool animated);

View file

@ -25,7 +25,7 @@ public:
virtual ~DecodedImageData();
virtual Optional<Gfx::IntRect> frame_rect([[maybe_unused]] size_t frame_index) const = 0;
virtual void paint([[maybe_unused]] DisplayListRecordingContext&, [[maybe_unused]] size_t frame_index, [[maybe_unused]] Gfx::IntRect dst_rect, [[maybe_unused]] Gfx::IntRect clip_rect, [[maybe_unused]] Gfx::ScalingMode scaling_mode) const = 0;
virtual void paint([[maybe_unused]] DisplayListRecordingContext&, [[maybe_unused]] size_t frame_index, [[maybe_unused]] Gfx::IntRect dst_rect, [[maybe_unused]] Gfx::ScalingMode scaling_mode) const = 0;
virtual Optional<Gfx::DecodedImageFrame> frame(size_t frame_index, Gfx::IntSize = {}) const = 0;
virtual int frame_duration(size_t frame_index) const = 0;

View file

@ -32,7 +32,7 @@ void FillRect::dump(StringBuilder& builder) const
void DrawScaledDecodedImageFrame::dump(StringBuilder& builder) const
{
builder.appendff(" dst_rect={} clip_rect={}", dst_rect, clip_rect);
builder.appendff(" dst_rect={}", dst_rect);
}
void DrawRepeatedDecodedImageFrame::dump(StringBuilder& builder) const

View file

@ -145,11 +145,10 @@ struct DrawScaledDecodedImageFrame {
static constexpr DisplayListCommandType command_type = DisplayListCommandType::DrawScaledDecodedImageFrame;
Gfx::IntRect dst_rect;
Gfx::IntRect clip_rect;
ImageFrameResourceId frame_id;
Gfx::ScalingMode scaling_mode;
[[nodiscard]] Gfx::IntRect bounding_rect() const { return clip_rect; }
[[nodiscard]] Gfx::IntRect bounding_rect() const { return dst_rect; }
void dump(StringBuilder&) const;
};

View file

@ -276,12 +276,11 @@ void DisplayListPlayerSkia::draw_scaled_decoded_image_frame(DrawScaledDecodedIma
return;
auto dst_rect = to_skia_rect(command.dst_rect);
auto clip_rect = to_skia_rect(command.clip_rect);
auto& canvas = surface().canvas();
SkPaint paint;
paint.setAntiAlias(true);
canvas.save();
canvas.clipRect(clip_rect, true);
canvas.clipRect(dst_rect, true);
canvas.drawImageRect(image.get(), dst_rect, to_skia_sampling_options(command.scaling_mode), &paint);
canvas.restore();
}
@ -858,6 +857,7 @@ void DisplayListPlayerSkia::add_rounded_rect_clip(AddRoundedRectClip const& comm
void DisplayListPlayerSkia::paint_nested_display_list(PaintNestedDisplayList const& command)
{
auto& canvas = surface().canvas();
canvas.save();
canvas.translate(command.rect.x(), command.rect.y());
ScrollStateSnapshot scroll_state_snapshot;
auto const& nested_display_list = resource_storage().display_list_resource(command.display_list_id);
@ -866,6 +866,7 @@ void DisplayListPlayerSkia::paint_nested_display_list(PaintNestedDisplayList con
nested_display_list.visual_context_tree,
scroll_state_snapshot,
nested_display_list.display_list->command_bytes());
canvas.restore();
}
void DisplayListPlayerSkia::compositor_scroll_node(CompositorScrollNode const&)

View file

@ -534,13 +534,12 @@ void DisplayListRecorder::draw_video_frame(Gfx::IntRect const& dst_rect, VideoFr
});
}
void DisplayListRecorder::draw_scaled_decoded_image_frame(Gfx::IntRect const& dst_rect, Gfx::IntRect const& clip_rect, Gfx::DecodedImageFrame frame, Gfx::ScalingMode scaling_mode)
void DisplayListRecorder::draw_scaled_decoded_image_frame(Gfx::IntRect const& dst_rect, Gfx::DecodedImageFrame frame, Gfx::ScalingMode scaling_mode)
{
if (dst_rect.is_empty())
return;
append_command(DrawScaledDecodedImageFrame {
.dst_rect = dst_rect,
.clip_rect = clip_rect,
.frame_id = resource_storage().add_image_frame(frame),
.scaling_mode = scaling_mode,
});

View file

@ -72,7 +72,7 @@ public:
void draw_rect(Gfx::IntRect const& rect, Color color, bool rough = false);
void draw_scaled_decoded_image_frame(Gfx::IntRect const& dst_rect, Gfx::IntRect const& clip_rect, Gfx::DecodedImageFrame frame, Gfx::ScalingMode scaling_mode = Gfx::ScalingMode::NearestNeighbor);
void draw_scaled_decoded_image_frame(Gfx::IntRect const& dst_rect, Gfx::DecodedImageFrame frame, Gfx::ScalingMode scaling_mode = Gfx::ScalingMode::NearestNeighbor);
void draw_compositor_surface(Gfx::IntRect const& dst_rect, CompositorSurfaceId, Gfx::ScalingMode scaling_mode = Gfx::ScalingMode::NearestNeighbor);
void draw_video_frame(Gfx::IntRect const& dst_rect, VideoFrameResourceId, RefPtr<Media::VideoFrame const>, Gfx::ScalingMode scaling_mode = Gfx::ScalingMode::NearestNeighbor);

View file

@ -76,8 +76,16 @@ void ImagePaintable::paint(DisplayListRecordingContext& context, PaintPhase phas
// https://drafts.csswg.org/css-images/#the-object-fit
auto object_fit = m_is_svg_image ? CSS::ObjectFit::Contain : computed_values().object_fit();
auto draw_rect = get_replaced_box_painting_area(*this, context, object_fit, bitmap_rect.size());
if (!draw_rect.is_empty())
decoded_image_data->paint(context, m_image_provider.current_frame_index(), draw_rect, image_int_rect_device_pixels, scaling_mode);
if (!draw_rect.is_empty()) {
auto draw_rect_needs_clip = !image_int_rect_device_pixels.contains(draw_rect);
if (draw_rect_needs_clip) {
context.display_list_recorder().save();
context.display_list_recorder().add_clip_rect(image_int_rect_device_pixels);
}
decoded_image_data->paint(context, m_image_provider.current_frame_index(), draw_rect, scaling_mode);
if (draw_rect_needs_clip)
context.display_list_recorder().restore();
}
}
if (selection_state() != SelectionState::None) {

View file

@ -58,7 +58,7 @@ void VideoPaintable::paint(DisplayListRecordingContext& context, PaintPhase phas
if (dst_rect.is_empty())
return;
auto scaling_mode = to_gfx_scaling_mode(computed_values().image_rendering(), frame.size(), dst_rect.size());
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, dst_rect, move(frame), scaling_mode);
context.display_list_recorder().draw_scaled_decoded_image_frame(dst_rect, move(frame), scaling_mode);
};
auto paint_video_frame = [&]() {

View file

@ -310,16 +310,13 @@ RefPtr<Gfx::PaintingSurface> SVGDecodedImageData::surface(size_t, Gfx::IntSize s
return render_to_surface(size);
}
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode) const
void SVGDecodedImageData::paint(DisplayListRecordingContext& context, size_t, Gfx::IntRect dst_rect, Gfx::ScalingMode) const
{
auto display_list = record_display_list(dst_rect.size(), context.display_list_recorder().resource_storage());
if (!display_list.has_value())
return;
context.display_list_recorder().save();
context.display_list_recorder().add_clip_rect(clip_rect);
context.display_list_recorder().paint_nested_display_list(*display_list, dst_rect);
context.display_list_recorder().restore();
}
}

View file

@ -42,7 +42,7 @@ public:
virtual size_t external_memory_size() const override;
virtual Optional<Gfx::IntRect> frame_rect(size_t frame_index) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::IntRect clip_rect, Gfx::ScalingMode scaling_mode) const override;
virtual void paint(DisplayListRecordingContext&, size_t frame_index, Gfx::IntRect dst_rect, Gfx::ScalingMode scaling_mode) const override;
private:
SVGDecodedImageData(GC::Ref<Page>, GC::Ref<SVGPageClient>, GC::Ref<DOM::Document>, GC::Ref<SVG::SVGSVGElement>);

View file

@ -1,5 +1,5 @@
<!doctype html>
<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-568">
<meta name="fuzzy" content="maxDifference=0-13;totalPixels=0-568">
<style>
body {
background-color: white;

View file

@ -0,0 +1,16 @@
AccumulatedVisualContext Tree:
[0] transform=[1,0,0,1,0,0] origin=(0,0) (ViewportPaintable(Viewport<#document>))
[1] scroll_frame_id=1 (ImagePaintable(ImageBox<IMG>))
DisplayList:
SaveLayer@0
CompositorWheelHitTestTarget@0 target_scroll_frame_index=0 rect=[0,0 800x600]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[0,0 800x40]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[0,0 800x40]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[0,0 40x40]
Save@1
AddClipRect@1 rect=[0,0 40x40]
DrawScaledDecodedImageFrame@1 dst_rect=[0,-20 40x80]
Restore@1
Restore@0

View file

@ -9,27 +9,23 @@ SaveLayer@0
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[8,13 784x200]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[8,13 784x200]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[8,13 300x200]
Save@1
AddClipRect@1 rect=[8,13 300x200]
PaintNestedDisplayList@1 rect=[8,13 300x200]
SaveLayer@0
CompositorWheelHitTestTarget@0 target_scroll_frame_index=0 rect=[0,0 300x200]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[0,0 300x200]
FillPath@1 path_bounding_rect=[5,0 55x56]
FillPath@1 path_bounding_rect=[1,50 55x55]
Save@1
AddClipRect@1 rect=[4,10 55x55]
SaveLayer@1
FillPath@1 path_bounding_rect=[5,0 55x56]
FillPath@1 path_bounding_rect=[1,50 55x55]
ApplyEffects@1 opacity=1 has_filter=false
PaintNestedDisplayList@1 rect=[4,10 55x55]
Translate@0 delta=[-4,-10]
FillPath@0 path_bounding_rect=[4,10 55x56]
Restore@1
PaintNestedDisplayList@1 rect=[8,13 300x200]
SaveLayer@0
CompositorWheelHitTestTarget@0 target_scroll_frame_index=0 rect=[0,0 300x200]
CompositorWheelHitTestTarget@1 target_scroll_frame_index=0 rect=[0,0 300x200]
FillPath@1 path_bounding_rect=[5,0 55x56]
FillPath@1 path_bounding_rect=[1,50 55x55]
Save@1
AddClipRect@1 rect=[4,10 55x55]
SaveLayer@1
FillPath@1 path_bounding_rect=[5,0 55x56]
FillPath@1 path_bounding_rect=[1,50 55x55]
ApplyEffects@1 opacity=1 has_filter=false
PaintNestedDisplayList@1 rect=[4,10 55x55]
Translate@0 delta=[-4,-10]
FillPath@0 path_bounding_rect=[4,10 55x56]
Restore@1
Restore@1
Restore@0
Restore@1
Restore@1
Restore@0
Restore@0

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
body {
font-size: 0;
margin: 0;
}
img {
display: block;
width: 40px;
height: 40px;
object-fit: cover;
}
</style>
<script>
asyncTest((done) => {
const image = document.createElement("img");
image.onload = () => {
println(internals.dumpDisplayList());
done();
};
document.body.appendChild(image);
image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQAQMAAADRPL3zAAAABlBMVEUwMDCAgIDHGf6uAAAAQ0lEQVRo3u3YMREAQAgDQRzg3+U7gDbl9+y1i4AMNdGrjBBCCCGEXJas84wQQgghhJwWW5kQQgghhPgnEkIIIYSQT1nvD3WWWfRskgAAAABJRU5ErkJggg==";
});
</script>