ladybird/Libraries/LibGfx/PainterSkia.cpp
Aliaksandr Kalenik be9ee28afc LibGfx: Stop caching Skia images during canvas playback
The decoded-frame Skia image cache is useful for display-list
rasterization because decoded image resources can be replayed over many
frames. The cache lets that path reuse SkImage wrappers and GPU-backed
copies instead of rebuilding them whenever the same resource is painted.

For canvas, commands are consumed into one backing surface and decoded
frames are already held by the command or paint style for the draw. A
per-painter cache does not match that usage model, and can keep decoded
frames and Skia images alive after the draw has consumed them.

This removes the cache from PainterSkia and drops the now-unused pruning
hook from CanvasCommandPlayer. With the cache gone, PainterSkia can hold
its painting surface directly instead of allocating a private Impl.
DisplayListPlayerSkia keeps owning the cache, so display-list
rasterization keeps the SkImage reuse behavior.
2026-06-16 00:38:30 +02:00

304 lines
12 KiB
C++

/*
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2024-2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
* Copyright (c) 2024, Lucien Fiorini <lucienfiorini@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#define AK_DONT_REPLACE_STD
#define SK_SUPPORT_UNSPANNED_APIS
#include <AK/GenericShorthands.h>
#include <AK/String.h>
#include <AK/TypeCasts.h>
#include <LibGfx/DecodedImageFrame.h>
#include <LibGfx/Filter.h>
#include <LibGfx/PainterSkia.h>
#include <LibGfx/PathSkia.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkCanvas.h>
#include <core/SkImage.h>
#include <core/SkPath.h>
#include <effects/SkBlurMaskFilter.h>
#include <effects/SkDashPathEffect.h>
#include <effects/SkGradientShader.h>
namespace Gfx {
static sk_sp<SkImage> sk_image_for_decoded_image_frame(DecodedImageFrame const& frame)
{
return sk_image_from_bitmap(frame.bitmap(), frame.color_space());
}
static void apply_paint_style(SkPaint& paint, PaintStyle const& style)
{
if (auto const& solid_color = as_if<SolidColorPaintStyle>(style)) {
paint.setColor(to_skia_color(solid_color->color()));
} else if (auto const& linear_gradient = as_if<Gfx::CanvasLinearGradientPaintStyle>(style)) {
auto const& color_stops = linear_gradient->color_stops();
Vector<SkColor> colors;
colors.ensure_capacity(color_stops.size());
Vector<SkScalar> positions;
positions.ensure_capacity(color_stops.size());
for (auto const& color_stop : color_stops) {
colors.unchecked_append(to_skia_color(color_stop.color));
positions.unchecked_append(color_stop.position);
}
Array points { to_skia_point(linear_gradient->start_point()), to_skia_point(linear_gradient->end_point()) };
SkMatrix matrix;
auto shader = SkGradientShader::MakeLinear(points.data(), colors.data(), positions.data(), color_stops.size(), SkTileMode::kClamp, 0, &matrix);
paint.setShader(shader);
} else if (auto const* radial_gradient = as_if<CanvasRadialGradientPaintStyle>(style)) {
auto const& color_stops = radial_gradient->color_stops();
Vector<SkColor> colors;
colors.ensure_capacity(color_stops.size());
Vector<SkScalar> positions;
positions.ensure_capacity(color_stops.size());
for (auto const& color_stop : color_stops) {
colors.unchecked_append(to_skia_color(color_stop.color));
positions.unchecked_append(color_stop.position);
}
auto start_center = radial_gradient->start_center();
auto end_center = radial_gradient->end_center();
auto start_radius = radial_gradient->start_radius();
auto end_radius = radial_gradient->end_radius();
auto start_sk_point = to_skia_point(start_center);
auto end_sk_point = to_skia_point(end_center);
SkMatrix matrix;
auto shader = SkGradientShader::MakeTwoPointConical(start_sk_point, start_radius, end_sk_point, end_radius, colors.data(), positions.data(), color_stops.size(), SkTileMode::kClamp, 0, &matrix);
paint.setShader(shader);
} else if (auto const* conic_gradient = as_if<CanvasConicGradientPaintStyle>(style)) {
auto const& color_stops = conic_gradient->color_stops();
Vector<SkColor> colors;
colors.ensure_capacity(color_stops.size());
Vector<SkScalar> positions;
positions.ensure_capacity(color_stops.size());
for (auto const& color_stop : color_stops) {
colors.unchecked_append(to_skia_color(color_stop.color));
positions.unchecked_append(color_stop.position);
}
auto center = conic_gradient->center();
auto start_angle_degrees = AK::to_degrees(conic_gradient->start_angle());
SkMatrix matrix;
auto shader = SkGradientShader::MakeSweep(center.x(), center.y(), colors.data(), positions.data(), color_stops.size(), SkTileMode::kClamp, start_angle_degrees, start_angle_degrees + 360, 0, &matrix);
paint.setShader(shader);
} else if (auto const* canvas_pattern = as_if<CanvasPatternPaintStyle>(style)) {
auto frame = canvas_pattern->image();
if (!frame.has_value())
return;
auto sk_image = sk_image_for_decoded_image_frame(*frame);
if (!sk_image)
return;
auto repetition = canvas_pattern->repetition();
auto repeat_x = first_is_one_of(repetition, CanvasPatternPaintStyle::Repetition::Repeat, CanvasPatternPaintStyle::Repetition::RepeatX);
auto repeat_y = first_is_one_of(repetition, CanvasPatternPaintStyle::Repetition::Repeat, CanvasPatternPaintStyle::Repetition::RepeatY);
// FIXME: Implement sampling configuration.
SkSamplingOptions sk_sampling_options { SkFilterMode::kLinear };
Optional<SkMatrix> transformation_matrix;
if (canvas_pattern->transform().has_value()) {
auto const& transform = canvas_pattern->transform().value();
transformation_matrix = SkMatrix::MakeAll(
transform.a(), transform.c(), transform.e(),
transform.b(), transform.d(), transform.f(),
0, 0, 1);
}
auto shader = sk_image->makeShader(
repeat_x ? SkTileMode::kRepeat : SkTileMode::kDecal,
repeat_y ? SkTileMode::kRepeat : SkTileMode::kDecal,
sk_sampling_options, transformation_matrix.has_value() ? &transformation_matrix.value() : nullptr);
paint.setShader(move(shader));
} else {
dbgln("FIXME: Unsupported PaintStyle");
}
}
static void apply_filter(SkPaint& paint, Gfx::Filter const& filter)
{
paint.setImageFilter(to_skia_image_filter(filter));
}
static SkPaint to_skia_paint(Gfx::PaintStyle const& style, Optional<Gfx::Filter const&> filter)
{
SkPaint paint;
apply_paint_style(paint, style);
if (filter.has_value())
apply_filter(paint, move(filter.value()));
return paint;
}
PainterSkia::PainterSkia(NonnullRefPtr<Gfx::PaintingSurface> painting_surface)
: m_painting_surface(move(painting_surface))
{
m_initial_save_count = m_painting_surface->canvas().save();
}
PainterSkia::~PainterSkia() = default;
void PainterSkia::clear_rect(Gfx::FloatRect const& rect, Gfx::Color color)
{
auto& canvas = m_painting_surface->canvas();
canvas.save();
canvas.clipRect(to_skia_rect(rect));
canvas.clear(to_skia_color(color));
canvas.restore();
}
void PainterSkia::fill_rect(Gfx::FloatRect const& rect, Color color)
{
SkPaint paint;
paint.setColor(to_skia_color(color));
auto& canvas = m_painting_surface->canvas();
canvas.drawRect(to_skia_rect(rect), paint);
}
void PainterSkia::draw_bitmap(Gfx::FloatRect const& dst_rect, Gfx::DecodedImageFrame const& source, Gfx::IntRect const& src_rect, Gfx::ScalingMode scaling_mode, Optional<Gfx::Filter> filter, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
SkPaint paint;
if (filter.has_value())
apply_filter(paint, filter.value());
paint.setAlpha(static_cast<u8>(global_alpha * 255));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_image = sk_image_for_decoded_image_frame(source);
if (!sk_image)
return;
auto& canvas = m_painting_surface->canvas();
canvas.drawImageRect(
sk_image.get(),
to_skia_rect(src_rect),
to_skia_rect(dst_rect),
to_skia_sampling_options(scaling_mode),
&paint,
SkCanvas::kStrict_SrcRectConstraint);
}
void PainterSkia::set_transform(Gfx::AffineTransform const& transform)
{
auto matrix = SkMatrix::MakeAll(
transform.a(), transform.c(), transform.e(),
transform.b(), transform.d(), transform.f(),
0, 0, 1);
auto& canvas = m_painting_surface->canvas();
canvas.setMatrix(matrix);
}
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::Color color, float thickness, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle cap_style, Gfx::Path::JoinStyle join_style, float miter_limit, Vector<float> const& dash_array, float dash_offset)
{
// Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing.
if (thickness <= 0)
return;
SkPaint paint;
paint.setAntiAlias(true);
paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, blur_radius / 2));
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(thickness);
paint.setColor(to_skia_color(color));
paint.setStrokeCap(to_skia_cap(cap_style));
paint.setStrokeJoin(to_skia_join(join_style));
paint.setStrokeMiter(miter_limit);
paint.setPathEffect(SkDashPathEffect::Make(dash_array.data(), dash_array.size(), dash_offset));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_path = to_skia_path(path);
auto& canvas = m_painting_surface->canvas();
canvas.drawPath(sk_path, paint);
}
void PainterSkia::stroke_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, Optional<Gfx::Filter> filter, float thickness, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::Path::CapStyle const& cap_style, Gfx::Path::JoinStyle const& join_style, float miter_limit, Vector<float> const& dash_array, float dash_offset)
{
// Skia treats zero thickness as a special case and will draw a hairline, while we want to draw nothing.
if (thickness <= 0)
return;
auto sk_path = to_skia_path(path);
auto paint = to_skia_paint(paint_style, filter);
paint.setAntiAlias(true);
float alpha = paint.getAlphaf();
paint.setAlphaf(alpha * global_alpha);
paint.setStyle(SkPaint::Style::kStroke_Style);
paint.setStrokeWidth(thickness);
paint.setStrokeCap(to_skia_cap(cap_style));
paint.setStrokeJoin(to_skia_join(join_style));
paint.setStrokeMiter(miter_limit);
paint.setPathEffect(SkDashPathEffect::Make(dash_array.data(), dash_array.size(), dash_offset));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto& canvas = m_painting_surface->canvas();
canvas.drawPath(sk_path, paint);
}
void PainterSkia::fill_path(Gfx::Path const& path, Gfx::Color color, Gfx::WindingRule winding_rule, float blur_radius, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator)
{
SkPaint paint;
paint.setAntiAlias(true);
paint.setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, blur_radius / 2));
paint.setColor(to_skia_color(color));
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto sk_path = to_skia_path(path);
sk_path.setFillType(to_skia_path_fill_type(winding_rule));
auto& canvas = m_painting_surface->canvas();
canvas.drawPath(sk_path, paint);
}
void PainterSkia::fill_path(Gfx::Path const& path, Gfx::PaintStyle const& paint_style, Optional<Gfx::Filter> filter, float global_alpha, Gfx::CompositingAndBlendingOperator compositing_and_blending_operator, Gfx::WindingRule winding_rule)
{
auto sk_path = to_skia_path(path);
sk_path.setFillType(to_skia_path_fill_type(winding_rule));
auto paint = to_skia_paint(paint_style, filter);
paint.setAntiAlias(true);
float alpha = paint.getAlphaf();
paint.setAlphaf(alpha * global_alpha);
paint.setBlender(to_skia_blender(compositing_and_blending_operator));
auto& canvas = m_painting_surface->canvas();
canvas.drawPath(sk_path, paint);
}
void PainterSkia::save()
{
auto& canvas = m_painting_surface->canvas();
canvas.save();
}
void PainterSkia::restore()
{
auto& canvas = m_painting_surface->canvas();
canvas.restore();
}
void PainterSkia::clip(Gfx::Path const& path, Gfx::WindingRule winding_rule)
{
auto sk_path = to_skia_path(path);
sk_path.setFillType(to_skia_path_fill_type(winding_rule));
auto& canvas = m_painting_surface->canvas();
canvas.clipPath(sk_path, SkClipOp::kIntersect, true);
}
void PainterSkia::reset()
{
auto& canvas = m_painting_surface->canvas();
canvas.restoreToCount(m_initial_save_count);
}
}