[web:canvaskit] switch to temporary SkPaint objects (flutter/engine#54818)

Do not eagerly create an `SkPaint` object that's strongly referenced by `CkPaint`. Instead, when a `Canvas.draw*` is called, create a temporary `SkPaint` object, pass it to Skia, then immediately delete it. This way there are no persistent `SkPaint` handles lurking in the system that transitively hold onto expensive native resources.

Addresses the `Paint` issue in https://github.com/flutter/flutter/issues/153678 in CanvasKit

Spot checking some benchmarks. Here's the effect of this PR on `draw_rect_variable_paint`. It's a bit of a stress test as it creates 300K distinct `Paint` objects to render 600 pictures (a typical ratio does not normally exceed ten paints to one picture). Even so, the effect of creating an extra `SkPaint` on every `draw*` command does not look significant. However, removing a dependency on the GC for 300K objects looks like a good trade-off.

## Before

Allocation stats:

```
  Paint Created: 300000
  Paint Deleted: 300000
  Paint Leaked: 300000
  Picture Created: 600
  Picture Deleted: 599
  Picture Leaked: 599
```

Performance stats:

```
windowRenderDuration: (samples: 98 clean/2 outliers/100 measured/300 total)
 | average: 4679.551020408163 μs
 | outlier average: 5100 μs
 | outlier/clean ratio: 1.0898481452084188x
 | noise: 3.11%

sceneBuildDuration: (samples: 98 clean/2 outliers/100 measured/300 total)
 | average: 4689.765306122449 μs
 | outlier average: 5100 μs
 | outlier/clean ratio: 1.087474461321549x
 | noise: 3.19%

drawFrameDuration: (samples: 97 clean/3 outliers/100 measured/300 total)
 | average: 8447.474226804125 μs
 | outlier average: 9332.666666666666 μs
 | outlier/clean ratio: 1.1047878236850721x
 | noise: 3.52%
```

## After

Allocation stats:

```
  Picture Created: 600
  Picture Deleted: 599
  Picture Leaked: 599
```

Performance stats:

```
windowRenderDuration: (samples: 97 clean/3 outliers/100 measured/300 total)
 | average: 4780.40206185567 μs
 | outlier average: 5133.666666666667 μs
 | outlier/clean ratio: 1.0738985131877936x
 | noise: 2.70%

sceneBuildDuration: (samples: 97 clean/3 outliers/100 measured/300 total)
 | average: 4787.6082474226805 μs
 | outlier average: 5133.666666666667 μs
 | outlier/clean ratio: 1.0722821085936345x
 | noise: 2.72%

drawFrameDuration: (samples: 97 clean/3 outliers/100 measured/300 total)
 | average: 8243.309278350516 μs
 | outlier average: 9033.333333333334 μs
 | outlier/clean ratio: 1.0958382159768851x
 | noise: 2.60%
```
This commit is contained in:
Yegor 2024-08-30 15:06:18 -07:00 committed by GitHub
parent f5ccc12041
commit 6bddf99dc0
14 changed files with 283 additions and 350 deletions

View File

@ -325,14 +325,6 @@ abstract class Paint {
factory Paint() => engine.renderer.createPaint(); factory Paint() => engine.renderer.createPaint();
factory Paint.from(Paint other) { factory Paint.from(Paint other) {
// This is less efficient than copying the underlying buffer or object but
// it's a reasonable default, as if a user wanted to implement a copy of a
// paint object themselves they are unable to do much better than this.
//
// TODO(matanlurey): Web team, if important to optimize, could:
// 1. Add a `engine.renderer.copyPaint` method.
// 2. Use the below code as the default implementation.
// 3. Have renderer-specific implementations override with optimized code.
final Paint paint = Paint(); final Paint paint = Paint();
paint paint
..blendMode = other.blendMode ..blendMode = other.blendMode

View File

@ -80,13 +80,16 @@ class CkCanvas {
CkPaint paint, CkPaint paint,
) { ) {
const double toDegrees = 180 / math.pi; const double toDegrees = 180 / math.pi;
final skPaint = paint.toSkPaint();
skCanvas.drawArc( skCanvas.drawArc(
toSkRect(oval), toSkRect(oval),
startAngle * toDegrees, startAngle * toDegrees,
sweepAngle * toDegrees, sweepAngle * toDegrees,
useCenter, useCenter,
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
// TODO(flar): CanvasKit does not expose sampling options available on SkCanvas.drawAtlas // TODO(flar): CanvasKit does not expose sampling options available on SkCanvas.drawAtlas
@ -98,23 +101,27 @@ class CkCanvas {
Uint32List? colors, Uint32List? colors,
ui.BlendMode blendMode, ui.BlendMode blendMode,
) { ) {
final skPaint = paint.toSkPaint();
skCanvas.drawAtlas( skCanvas.drawAtlas(
atlas.skImage, atlas.skImage,
rects, rects,
rstTransforms, rstTransforms,
paint.skiaObject, skPaint,
toSkBlendMode(blendMode), toSkBlendMode(blendMode),
colors, colors,
); );
skPaint.delete();
} }
void drawCircle(ui.Offset c, double radius, CkPaint paint) { void drawCircle(ui.Offset c, double radius, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawCircle( skCanvas.drawCircle(
c.dx, c.dx,
c.dy, c.dy,
radius, radius,
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawColor(ui.Color color, ui.BlendMode blendMode) { void drawColor(ui.Color color, ui.BlendMode blendMode) {
@ -125,15 +132,18 @@ class CkCanvas {
} }
void drawDRRect(ui.RRect outer, ui.RRect inner, CkPaint paint) { void drawDRRect(ui.RRect outer, ui.RRect inner, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawDRRect( skCanvas.drawDRRect(
toSkRRect(outer), toSkRRect(outer),
toSkRRect(inner), toSkRRect(inner),
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawImage(CkImage image, ui.Offset offset, CkPaint paint) { void drawImage(CkImage image, ui.Offset offset, CkPaint paint) {
final ui.FilterQuality filterQuality = paint.filterQuality; final ui.FilterQuality filterQuality = paint.filterQuality;
final skPaint = paint.toSkPaint();
if (filterQuality == ui.FilterQuality.high) { if (filterQuality == ui.FilterQuality.high) {
skCanvas.drawImageCubic( skCanvas.drawImageCubic(
image.skImage, image.skImage,
@ -141,7 +151,7 @@ class CkCanvas {
offset.dy, offset.dy,
_kMitchellNetravali_B, _kMitchellNetravali_B,
_kMitchellNetravali_C, _kMitchellNetravali_C,
paint.skiaObject, skPaint,
); );
} else { } else {
skCanvas.drawImageOptions( skCanvas.drawImageOptions(
@ -150,13 +160,15 @@ class CkCanvas {
offset.dy, offset.dy,
toSkFilterMode(filterQuality), toSkFilterMode(filterQuality),
toSkMipmapMode(filterQuality), toSkMipmapMode(filterQuality),
paint.skiaObject, skPaint,
); );
} }
skPaint.delete();
} }
void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) { void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) {
final ui.FilterQuality filterQuality = paint.filterQuality; final ui.FilterQuality filterQuality = paint.filterQuality;
final skPaint = paint.toSkPaint();
if (filterQuality == ui.FilterQuality.high) { if (filterQuality == ui.FilterQuality.high) {
skCanvas.drawImageRectCubic( skCanvas.drawImageRectCubic(
image.skImage, image.skImage,
@ -164,7 +176,7 @@ class CkCanvas {
toSkRect(dst), toSkRect(dst),
_kMitchellNetravali_B, _kMitchellNetravali_B,
_kMitchellNetravali_C, _kMitchellNetravali_C,
paint.skiaObject, skPaint,
); );
} else { } else {
skCanvas.drawImageRectOptions( skCanvas.drawImageRectOptions(
@ -173,41 +185,50 @@ class CkCanvas {
toSkRect(dst), toSkRect(dst),
toSkFilterMode(filterQuality), toSkFilterMode(filterQuality),
toSkMipmapMode(filterQuality), toSkMipmapMode(filterQuality),
paint.skiaObject, skPaint,
); );
} }
skPaint.delete();
} }
void drawImageNine( void drawImageNine(
CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) { CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawImageNine( skCanvas.drawImageNine(
image.skImage, image.skImage,
toSkRect(center), toSkRect(center),
toSkRect(dst), toSkRect(dst),
toSkFilterMode(paint.filterQuality), toSkFilterMode(paint.filterQuality),
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawLine(ui.Offset p1, ui.Offset p2, CkPaint paint) { void drawLine(ui.Offset p1, ui.Offset p2, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawLine( skCanvas.drawLine(
p1.dx, p1.dx,
p1.dy, p1.dy,
p2.dx, p2.dx,
p2.dy, p2.dy,
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawOval(ui.Rect rect, CkPaint paint) { void drawOval(ui.Rect rect, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawOval( skCanvas.drawOval(
toSkRect(rect), toSkRect(rect),
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawPaint(CkPaint paint) { void drawPaint(CkPaint paint) {
skCanvas.drawPaint(paint.skiaObject); final skPaint = paint.toSkPaint();
skCanvas.drawPaint(skPaint);
skPaint.delete();
} }
void drawParagraph(CkParagraph paragraph, ui.Offset offset) { void drawParagraph(CkParagraph paragraph, ui.Offset offset) {
@ -219,7 +240,9 @@ class CkCanvas {
} }
void drawPath(CkPath path, CkPaint paint) { void drawPath(CkPath path, CkPaint paint) {
skCanvas.drawPath(path.skiaObject, paint.skiaObject); final skPaint = paint.toSkPaint();
skCanvas.drawPath(path.skiaObject, skPaint);
skPaint.delete();
} }
void drawPicture(CkPicture picture) { void drawPicture(CkPicture picture) {
@ -228,22 +251,28 @@ class CkCanvas {
} }
void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) {
final skPaint = paint.toSkPaint();
skCanvas.drawPoints( skCanvas.drawPoints(
toSkPointMode(pointMode), toSkPointMode(pointMode),
points, points,
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawRRect(ui.RRect rrect, CkPaint paint) { void drawRRect(ui.RRect rrect, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawRRect( skCanvas.drawRRect(
toSkRRect(rrect), toSkRRect(rrect),
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void drawRect(ui.Rect rect, CkPaint paint) { void drawRect(ui.Rect rect, CkPaint paint) {
skCanvas.drawRect(toSkRect(rect), paint.skiaObject); final skPaint = paint.toSkPaint();
skCanvas.drawRect(toSkRect(rect), skPaint);
skPaint.delete();
} }
void drawShadow( void drawShadow(
@ -254,11 +283,13 @@ class CkCanvas {
void drawVertices( void drawVertices(
CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) { CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawVertices( skCanvas.drawVertices(
vertices.skiaObject, vertices.skiaObject,
toSkBlendMode(blendMode), toSkBlendMode(blendMode),
paint.skiaObject, skPaint,
); );
skPaint.delete();
} }
void restore() { void restore() {
@ -278,16 +309,20 @@ class CkCanvas {
} }
void saveLayer(ui.Rect bounds, CkPaint? paint) { void saveLayer(ui.Rect bounds, CkPaint? paint) {
final skPaint = paint?.toSkPaint();
skCanvas.saveLayer( skCanvas.saveLayer(
paint?.skiaObject, skPaint,
toSkRect(bounds), toSkRect(bounds),
null, null,
null, null,
); );
skPaint?.delete();
} }
void saveLayerWithoutBounds(CkPaint? paint) { void saveLayerWithoutBounds(CkPaint? paint) {
skCanvas.saveLayer(paint?.skiaObject, null, null, null); final skPaint = paint?.toSkPaint();
skCanvas.saveLayer(skPaint, null, null, null);
skPaint?.delete();
} }
void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter, void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter,
@ -298,13 +333,15 @@ class CkCanvas {
} else { } else {
convertible = filter as CkManagedSkImageFilterConvertible; convertible = filter as CkManagedSkImageFilterConvertible;
} }
convertible.imageFilter((SkImageFilter filter) { convertible.withSkImageFilter((SkImageFilter filter) {
final skPaint = paint?.toSkPaint();
skCanvas.saveLayer( skCanvas.saveLayer(
paint?.skiaObject, skPaint,
toSkRect(bounds), toSkRect(bounds),
filter, filter,
0, 0,
); );
skPaint?.delete();
}); });
} }

View File

@ -1516,6 +1516,9 @@ class SkImageFilter {}
extension SkImageFilterExtension on SkImageFilter { extension SkImageFilterExtension on SkImageFilter {
external JSVoid delete(); external JSVoid delete();
@JS('isDeleted')
external JSBoolean _isDeleted();
bool isDeleted() => _isDeleted().toDart;
@JS('getOutputBounds') @JS('getOutputBounds')
external JSInt32Array _getOutputBounds(JSFloat32Array bounds); external JSInt32Array _getOutputBounds(JSFloat32Array bounds);

View File

@ -71,7 +71,7 @@ abstract class CkColorFilter implements CkManagedSkImageFilterConvertible {
SkColorFilter _initRawColorFilter(); SkColorFilter _initRawColorFilter();
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
// Since ColorFilter has a const constructor it cannot store dynamically // Since ColorFilter has a const constructor it cannot store dynamically
// created Skia objects. Therefore a new SkImageFilter is created every time // created Skia objects. Therefore a new SkImageFilter is created every time
// it's used. However, once used it's no longer needed, so it's deleted // it's used. However, once used it's no longer needed, so it's deleted

View File

@ -300,7 +300,6 @@ CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) {
ui.Rect.fromLTWH(0, 0, targetWidth!.toDouble(), targetHeight!.toDouble()), ui.Rect.fromLTWH(0, 0, targetWidth!.toDouble(), targetHeight!.toDouble()),
paint, paint,
); );
paint.dispose();
final CkPicture picture = recorder.endRecording(); final CkPicture picture = recorder.endRecording();
final ui.Image finalImage = picture.toImageSync(targetWidth, targetHeight); final ui.Image finalImage = picture.toImageSync(targetWidth, targetHeight);

View File

@ -10,7 +10,6 @@ import 'package:ui/ui.dart' as ui;
import '../util.dart'; import '../util.dart';
import 'canvaskit_api.dart'; import 'canvaskit_api.dart';
import 'color_filter.dart'; import 'color_filter.dart';
import 'native_memory.dart';
typedef SkImageFilterBorrow = void Function(SkImageFilter); typedef SkImageFilterBorrow = void Function(SkImageFilter);
@ -22,7 +21,12 @@ typedef SkImageFilterBorrow = void Function(SkImageFilter);
/// ///
/// Currently implemented by [CkImageFilter] and [CkColorFilter]. /// Currently implemented by [CkImageFilter] and [CkColorFilter].
abstract class CkManagedSkImageFilterConvertible implements ui.ImageFilter { abstract class CkManagedSkImageFilterConvertible implements ui.ImageFilter {
void imageFilter(SkImageFilterBorrow borrow); /// Creates a temporary [SkImageFilter], passes it to [borrow], and then
/// immediately deletes it.
///
/// [SkImageFilter] objects are not kept around so that their memory is
/// reclaimed immediately, rather than waiting for the GC cycle.
void withSkImageFilter(SkImageFilterBorrow borrow);
Matrix4 get transform; Matrix4 get transform;
} }
@ -56,22 +60,15 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible {
} }
class CkColorFilterImageFilter extends CkImageFilter { class CkColorFilterImageFilter extends CkImageFilter {
CkColorFilterImageFilter({required this.colorFilter}) : super._() { CkColorFilterImageFilter({required this.colorFilter}) : super._();
final SkImageFilter skImageFilter = colorFilter.initRawImageFilter();
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.color');
}
final CkColorFilter colorFilter; final CkColorFilter colorFilter;
late final UniqueRef<SkImageFilter> _ref;
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject); final skImageFilter = colorFilter.initRawImageFilter();
} borrow(skImageFilter);
skImageFilter.delete();
void dispose() {
_ref.dispose();
} }
@override @override
@ -93,7 +90,14 @@ class CkColorFilterImageFilter extends CkImageFilter {
class _CkBlurImageFilter extends CkImageFilter { class _CkBlurImageFilter extends CkImageFilter {
_CkBlurImageFilter( _CkBlurImageFilter(
{required this.sigmaX, required this.sigmaY, required this.tileMode}) {required this.sigmaX, required this.sigmaY, required this.tileMode})
: super._() { : super._();
final double sigmaX;
final double sigmaY;
final ui.TileMode tileMode;
@override
void withSkImageFilter(SkImageFilterBorrow borrow) {
/// Return the identity matrix when both sigmaX and sigmaY are 0. Replicates /// Return the identity matrix when both sigmaX and sigmaY are 0. Replicates
/// effect of applying no filter /// effect of applying no filter
final SkImageFilter skImageFilter; final SkImageFilter skImageFilter;
@ -110,18 +114,9 @@ class _CkBlurImageFilter extends CkImageFilter {
null, null,
); );
} }
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.blur');
}
final double sigmaX; borrow(skImageFilter);
final double sigmaY; skImageFilter.delete();
final ui.TileMode tileMode;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
} }
@override @override
@ -149,25 +144,22 @@ class _CkMatrixImageFilter extends CkImageFilter {
{required Float64List matrix, required this.filterQuality}) {required Float64List matrix, required this.filterQuality})
: matrix = Float64List.fromList(matrix), : matrix = Float64List.fromList(matrix),
_transform = Matrix4.fromFloat32List(toMatrix32(matrix)), _transform = Matrix4.fromFloat32List(toMatrix32(matrix)),
super._() { super._();
final SkImageFilter skImageFilter =
canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat64(matrix),
toSkFilterOptions(filterQuality),
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.matrix');
}
final Float64List matrix; final Float64List matrix;
final ui.FilterQuality filterQuality; final ui.FilterQuality filterQuality;
final Matrix4 _transform; final Matrix4 _transform;
late final UniqueRef<SkImageFilter> _ref;
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject); final skImageFilter =
canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat64(matrix),
toSkFilterOptions(filterQuality),
null,
);
borrow(skImageFilter);
skImageFilter.delete();
} }
@override @override
@ -192,23 +184,20 @@ class _CkMatrixImageFilter extends CkImageFilter {
class _CkDilateImageFilter extends CkImageFilter { class _CkDilateImageFilter extends CkImageFilter {
_CkDilateImageFilter({required this.radiusX, required this.radiusY}) _CkDilateImageFilter({required this.radiusX, required this.radiusY})
: super._() { : super._();
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeDilate(
radiusX,
radiusY,
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.dilate');
}
final double radiusX; final double radiusX;
final double radiusY; final double radiusY;
late final UniqueRef<SkImageFilter> _ref;
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject); final skImageFilter = canvasKit.ImageFilter.MakeDilate(
radiusX,
radiusY,
null,
);
borrow(skImageFilter);
skImageFilter.delete();
} }
@override @override
@ -232,23 +221,20 @@ class _CkDilateImageFilter extends CkImageFilter {
class _CkErodeImageFilter extends CkImageFilter { class _CkErodeImageFilter extends CkImageFilter {
_CkErodeImageFilter({required this.radiusX, required this.radiusY}) _CkErodeImageFilter({required this.radiusX, required this.radiusY})
: super._() { : super._();
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeErode(
radiusX,
radiusY,
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.erode');
}
final double radiusX; final double radiusX;
final double radiusY; final double radiusY;
late final UniqueRef<SkImageFilter> _ref;
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject); final skImageFilter = canvasKit.ImageFilter.MakeErode(
radiusX,
radiusY,
null,
);
borrow(skImageFilter);
skImageFilter.delete();
} }
@override @override
@ -272,27 +258,23 @@ class _CkErodeImageFilter extends CkImageFilter {
class _CkComposeImageFilter extends CkImageFilter { class _CkComposeImageFilter extends CkImageFilter {
_CkComposeImageFilter({required this.outer, required this.inner}) _CkComposeImageFilter({required this.outer, required this.inner})
: super._() { : super._();
outer.imageFilter((SkImageFilter outerFilter) {
inner.imageFilter((SkImageFilter innerFilter) {
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeCompose(
outerFilter,
innerFilter,
);
_ref = UniqueRef<SkImageFilter>(
this, skImageFilter, 'ImageFilter.compose');
});
});
}
final CkImageFilter outer; final CkImageFilter outer;
final CkImageFilter inner; final CkImageFilter inner;
late final UniqueRef<SkImageFilter> _ref;
@override @override
void imageFilter(SkImageFilterBorrow borrow) { void withSkImageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject); outer.withSkImageFilter((skOuter) {
inner.withSkImageFilter((skInner) {
final skImageFilter = canvasKit.ImageFilter.MakeCompose(
skOuter,
skInner,
);
borrow(skImageFilter);
skImageFilter.delete();
});
});
} }
@override @override

View File

@ -187,7 +187,6 @@ class BackdropFilterEngineLayer extends ContainerLayer
// single canvas, the backdrop filter will be applied multiple times. // single canvas, the backdrop filter will be applied multiple times.
final CkCanvas currentCanvas = paintContext.leafNodesCanvas!; final CkCanvas currentCanvas = paintContext.leafNodesCanvas!;
currentCanvas.saveLayerWithFilter(paintBounds, _filter, paint); currentCanvas.saveLayerWithFilter(paintBounds, _filter, paint);
paint.dispose();
paintChildren(paintContext); paintChildren(paintContext);
currentCanvas.restore(); currentCanvas.restore();
} }
@ -349,7 +348,6 @@ class OpacityEngineLayer extends ContainerLayer
final ui.Rect saveLayerBounds = paintBounds.shift(-_offset); final ui.Rect saveLayerBounds = paintBounds.shift(-_offset);
paintContext.internalNodesCanvas.saveLayer(saveLayerBounds, paint); paintContext.internalNodesCanvas.saveLayer(saveLayerBounds, paint);
paint.dispose();
paintChildren(paintContext); paintChildren(paintContext);
// Restore twice: once for the translate and once for the saveLayer. // Restore twice: once for the translate and once for the saveLayer.
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
@ -419,16 +417,17 @@ class ImageFilterEngineLayer extends ContainerLayer
} }
final ui.Rect childPaintBounds = final ui.Rect childPaintBounds =
prerollChildren(prerollContext, childMatrix); prerollChildren(prerollContext, childMatrix);
convertible.imageFilter((SkImageFilter filter) {
// If the filter is a ColorFilter, the extended paint bounds will be the
// entire screen, which is not what we want.
if (_filter is ui.ColorFilter) { if (_filter is ui.ColorFilter) {
// If the filter is a ColorFilter, the extended paint bounds will be the
// entire screen, which is not what we want.
paintBounds = childPaintBounds; paintBounds = childPaintBounds;
} else { } else {
paintBounds = convertible.withSkImageFilter((skFilter) {
rectFromSkIRect(filter.getOutputBounds(toSkRect(childPaintBounds))); paintBounds = rectFromSkIRect(
skFilter.getOutputBounds(toSkRect(childPaintBounds)),
);
});
} }
});
prerollContext.mutatorsStack.pop(); prerollContext.mutatorsStack.pop();
} }
@ -442,7 +441,6 @@ class ImageFilterEngineLayer extends ContainerLayer
final CkPaint paint = CkPaint(); final CkPaint paint = CkPaint();
paint.imageFilter = _filter; paint.imageFilter = _filter;
paintContext.internalNodesCanvas.saveLayer(paintBounds, paint); paintContext.internalNodesCanvas.saveLayer(paintBounds, paint);
paint.dispose();
paintChildren(paintContext); paintChildren(paintContext);
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
@ -479,7 +477,6 @@ class ShaderMaskEngineLayer extends ContainerLayer
paintContext.leafNodesCanvas!.drawRect( paintContext.leafNodesCanvas!.drawRect(
ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), paint); ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), paint);
paint.dispose();
paintContext.leafNodesCanvas!.restore(); paintContext.leafNodesCanvas!.restore();
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
@ -547,7 +544,6 @@ class ColorFilterEngineLayer extends ContainerLayer
paintChildren(paintContext); paintChildren(paintContext);
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
paintContext.internalNodesCanvas.restore(); paintContext.internalNodesCanvas.restore();
paint.dispose();
} }
} }

View File

@ -5,25 +5,14 @@
import 'package:ui/ui.dart' as ui; import 'package:ui/ui.dart' as ui;
import 'canvaskit_api.dart'; import 'canvaskit_api.dart';
import 'native_memory.dart';
/// The CanvasKit implementation of [ui.MaskFilter]. /// Creates and returns a [SkMaskFilter] that applies a blur effect.
class CkMaskFilter { ///
CkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma) /// It is the responsibility of the caller to delete the returned Skia object.
: _blurStyle = blurStyle, SkMaskFilter createBlurSkMaskFilter(ui.BlurStyle blurStyle, double sigma) {
_sigma = sigma { return canvasKit.MaskFilter.MakeBlur(
final SkMaskFilter skMaskFilter = canvasKit.MaskFilter.MakeBlur( toSkBlurStyle(blurStyle),
toSkBlurStyle(_blurStyle), sigma,
_sigma, true,
true, )!;
)!;
_ref = UniqueRef<SkMaskFilter>(this, skMaskFilter, 'MaskFilter');
}
final ui.BlurStyle _blurStyle;
final double _sigma;
late final UniqueRef<SkMaskFilter> _ref;
SkMaskFilter get skiaObject => _ref.nativeObject;
} }

View File

@ -21,116 +21,83 @@ import 'shader.dart';
/// ///
/// This class is backed by a Skia object that must be explicitly /// This class is backed by a Skia object that must be explicitly
/// deleted to avoid a memory leak. This is done by extending [SkiaObject]. /// deleted to avoid a memory leak. This is done by extending [SkiaObject].
// TODO(154281): try to unify with SkwasmPaint
class CkPaint implements ui.Paint { class CkPaint implements ui.Paint {
CkPaint() : skiaObject = SkPaint() { CkPaint();
skiaObject.setAntiAlias(_isAntiAlias);
skiaObject.setColorInt(_defaultPaintColor);
_ref = UniqueRef<SkPaint>(this, skiaObject, 'Paint');
}
final SkPaint skiaObject; /// Creates a new [SkPaint] object and returns it.
late final UniqueRef<SkPaint> _ref;
CkManagedSkImageFilterConvertible? _imageFilter;
static const int _defaultPaintColor = 0xFF000000;
/// Returns the native reference to the underlying [SkPaint] object.
/// ///
/// This should only be used in tests. /// The caller is responsible for deleting the returned object when it's no
@visibleForTesting /// longer needed.
UniqueRef<SkPaint> get debugRef => _ref; SkPaint toSkPaint() {
final skPaint = SkPaint();
skPaint.setAntiAlias(isAntiAlias);
skPaint.setBlendMode(toSkBlendMode(blendMode));
skPaint.setStyle(toSkPaintStyle(style));
skPaint.setStrokeWidth(strokeWidth);
skPaint.setStrokeCap(toSkStrokeCap(strokeCap));
skPaint.setStrokeJoin(toSkStrokeJoin(strokeJoin));
skPaint.setColorInt(_colorValue);
skPaint.setStrokeMiter(strokeMiterLimit);
@override final effectiveColorFilter = _effectiveColorFilter;
ui.BlendMode get blendMode => _blendMode; if (effectiveColorFilter != null) {
@override skPaint.setColorFilter(effectiveColorFilter.skiaObject);
set blendMode(ui.BlendMode value) {
if (_blendMode == value) {
return;
} }
_blendMode = value;
skiaObject.setBlendMode(toSkBlendMode(value)); final shader = _shader;
if (shader != null) {
skPaint.setShader(shader.getSkShader(filterQuality));
}
final localMaskFilter = maskFilter;
if (localMaskFilter != null) {
// CanvasKit returns `null` if the sigma is `0` or infinite.
if (localMaskFilter.webOnlySigma.isFinite && localMaskFilter.webOnlySigma > 0) {
skPaint.setMaskFilter(createBlurSkMaskFilter(
localMaskFilter.webOnlyBlurStyle,
localMaskFilter.webOnlySigma,
));
}
}
final localImageFilter = _imageFilter;
if (localImageFilter != null) {
localImageFilter.withSkImageFilter((skImageFilter) {
skPaint.setImageFilter(skImageFilter);
});
}
return skPaint;
} }
ui.BlendMode _blendMode = ui.BlendMode.srcOver; @override
ui.BlendMode blendMode = ui.BlendMode.srcOver;
@override @override
ui.PaintingStyle get style => _style; ui.PaintingStyle style = ui.PaintingStyle.fill;
@override @override
set style(ui.PaintingStyle value) { double strokeWidth = 0.0;
if (_style == value) {
return;
}
_style = value;
skiaObject.setStyle(toSkPaintStyle(value));
}
ui.PaintingStyle _style = ui.PaintingStyle.fill;
@override @override
double get strokeWidth => _strokeWidth; ui.StrokeCap strokeCap = ui.StrokeCap.butt;
@override
set strokeWidth(double value) {
if (_strokeWidth == value) {
return;
}
_strokeWidth = value;
skiaObject.setStrokeWidth(value);
}
double _strokeWidth = 0.0;
@override @override
ui.StrokeCap get strokeCap => _strokeCap; ui.StrokeJoin strokeJoin = ui.StrokeJoin.miter;
@override
set strokeCap(ui.StrokeCap value) {
if (_strokeCap == value) {
return;
}
_strokeCap = value;
skiaObject.setStrokeCap(toSkStrokeCap(value));
}
ui.StrokeCap _strokeCap = ui.StrokeCap.butt;
@override @override
ui.StrokeJoin get strokeJoin => _strokeJoin; bool isAntiAlias = true;
@override
set strokeJoin(ui.StrokeJoin value) {
if (_strokeJoin == value) {
return;
}
_strokeJoin = value;
skiaObject.setStrokeJoin(toSkStrokeJoin(value));
}
ui.StrokeJoin _strokeJoin = ui.StrokeJoin.miter;
@override @override
bool get isAntiAlias => _isAntiAlias; ui.Color get color => ui.Color(_colorValue);
@override
set isAntiAlias(bool value) {
if (_isAntiAlias == value) {
return;
}
_isAntiAlias = value;
skiaObject.setAntiAlias(value);
}
bool _isAntiAlias = true;
@override
ui.Color get color => ui.Color(_color);
@override @override
set color(ui.Color value) { set color(ui.Color value) {
if (_color == value.value) { _colorValue = value.value;
return;
}
_color = value.value;
skiaObject.setColorInt(value.value);
} }
int _color = _defaultPaintColor; static const int _defaultPaintColorValue = 0xFF000000;
int _colorValue = _defaultPaintColorValue;
@override @override
bool get invertColors => _invertColors; bool get invertColors => _invertColors;
@ -152,7 +119,6 @@ class CkPaint implements ui.Paint {
); );
} }
} }
skiaObject.setColorFilter(_effectiveColorFilter?.skiaObject);
_invertColors = value; _invertColors = value;
} }
@ -170,52 +136,15 @@ class CkPaint implements ui.Paint {
return; return;
} }
_shader = value as CkShader?; _shader = value as CkShader?;
skiaObject.setShader(_shader?.getSkShader(_filterQuality));
} }
CkShader? _shader; CkShader? _shader;
@override @override
ui.MaskFilter? get maskFilter => _maskFilter; ui.MaskFilter? maskFilter;
@override
set maskFilter(ui.MaskFilter? value) {
if (value == _maskFilter) {
return;
}
_maskFilter = value;
if (value != null) {
// CanvasKit returns `null` if the sigma is `0` or infinite.
if (!(value.webOnlySigma.isFinite && value.webOnlySigma > 0)) {
// Don't create a [CkMaskFilter].
_ckMaskFilter = null;
} else {
_ckMaskFilter = CkMaskFilter.blur(
value.webOnlyBlurStyle,
value.webOnlySigma,
);
}
} else {
_ckMaskFilter = null;
}
skiaObject.setMaskFilter(_ckMaskFilter?.skiaObject);
}
ui.MaskFilter? _maskFilter;
CkMaskFilter? _ckMaskFilter;
@override @override
ui.FilterQuality get filterQuality => _filterQuality; ui.FilterQuality filterQuality = ui.FilterQuality.none;
@override
set filterQuality(ui.FilterQuality value) {
if (_filterQuality == value) {
return;
}
_filterQuality = value;
skiaObject.setShader(_shader?.getSkShader(value));
}
ui.FilterQuality _filterQuality = ui.FilterQuality.none;
EngineColorFilter? _engineColorFilter;
@override @override
ui.ColorFilter? get colorFilter => _engineColorFilter; ui.ColorFilter? get colorFilter => _engineColorFilter;
@ -244,27 +173,18 @@ class CkPaint implements ui.Paint {
); );
} }
} }
skiaObject.setColorFilter(_effectiveColorFilter?.skiaObject);
} }
/// The original color filter objects passed by the framework.
EngineColorFilter? _engineColorFilter;
/// The effective color filter. /// The effective color filter.
/// ///
/// This is a combination of the `colorFilter` and `invertColors` properties. /// This is a combination of the `colorFilter` and `invertColors` properties.
ManagedSkColorFilter? _effectiveColorFilter; ManagedSkColorFilter? _effectiveColorFilter;
@override @override
double get strokeMiterLimit => _strokeMiterLimit; double strokeMiterLimit = 4.0;
@override
set strokeMiterLimit(double value) {
if (_strokeMiterLimit == value) {
return;
}
_strokeMiterLimit = value;
skiaObject.setStrokeMiter(value);
}
double _strokeMiterLimit = 0.0;
@override @override
ui.ImageFilter? get imageFilter => _imageFilter; ui.ImageFilter? get imageFilter => _imageFilter;
@ -273,29 +193,15 @@ class CkPaint implements ui.Paint {
if (_imageFilter == value) { if (_imageFilter == value) {
return; return;
} }
final CkManagedSkImageFilterConvertible? filter;
if (value is ui.ColorFilter) { if (value is ui.ColorFilter) {
filter = createCkColorFilter(value as EngineColorFilter); _imageFilter = createCkColorFilter(value as EngineColorFilter);
} else {
_imageFilter = value as CkManagedSkImageFilterConvertible?;
} }
else {
filter = value as CkManagedSkImageFilterConvertible?;
}
if (filter != null) {
filter.imageFilter((SkImageFilter skImageFilter) {
skiaObject.setImageFilter(skImageFilter);
});
}
_imageFilter = filter;
} }
/// Disposes of this paint object. CkManagedSkImageFilterConvertible? _imageFilter;
///
/// This object cannot be used again after calling this method.
void dispose() {
_ref.dispose();
}
// Must be kept in sync with the default in paint.cc. // Must be kept in sync with the default in paint.cc.
static const double _kStrokeMiterLimitDefault = 4.0; static const double _kStrokeMiterLimitDefault = 4.0;

View File

@ -1169,38 +1169,47 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
return _styleStack.last; return _styleStack.last;
} }
// Used as the paint for background or foreground in the text style when static SkPaint createForegroundPaint(CkTextStyle style) {
// the other one is not specified. CanvasKit either both background and final SkPaint foreground;
// foreground paints specified, or neither, but Flutter allows one of them if (style.foreground != null) {
// to go unspecified. foreground = style.foreground!.toSkPaint();
// } else {
// This object is never deleted. It is effectively a static global constant. foreground = SkPaint();
// Therefore it doesn't need to be wrapped in CkPaint. foreground.setColorInt(
static final SkPaint _defaultTextForeground = SkPaint(); style.color?.value ?? 0xFF000000,
static final SkPaint _defaultTextBackground = SkPaint() );
..setColorInt(0x00000000); }
return foreground;
}
static SkPaint createBackgroundPaint(CkTextStyle style) {
final SkPaint background;
if (style.background != null) {
background = style.background!.toSkPaint();
} else {
background = SkPaint()
..setColorInt(0x00000000);
}
return background;
}
@override @override
void pushStyle(ui.TextStyle style) { void pushStyle(ui.TextStyle leafStyle) {
final CkTextStyle baseStyle = _peekStyle(); leafStyle as CkTextStyle;
final CkTextStyle ckStyle = style as CkTextStyle;
final CkTextStyle skStyle = baseStyle.mergeWith(ckStyle);
_styleStack.add(skStyle);
if (skStyle.foreground != null || skStyle.background != null) {
SkPaint? foreground = skStyle.foreground?.skiaObject;
if (foreground == null) {
_defaultTextForeground.setColorInt(
skStyle.color?.value ?? 0xFF000000,
);
foreground = _defaultTextForeground;
}
final SkPaint background = final CkTextStyle baseStyle = _peekStyle();
skStyle.background?.skiaObject ?? _defaultTextBackground; final CkTextStyle mergedStyle = baseStyle.mergeWith(leafStyle);
_styleStack.add(mergedStyle);
if (mergedStyle.foreground != null || mergedStyle.background != null) {
final foreground = createForegroundPaint(mergedStyle);
final background = createBackgroundPaint(mergedStyle);
_paragraphBuilder.pushPaintStyle( _paragraphBuilder.pushPaintStyle(
skStyle.skTextStyle, foreground, background); mergedStyle.skTextStyle, foreground, background);
foreground.delete();
background.delete();
} else { } else {
_paragraphBuilder.pushStyle(skStyle.skTextStyle); _paragraphBuilder.pushStyle(mergedStyle.skTextStyle);
} }
} }
} }

View File

@ -148,7 +148,7 @@ class SurfacePaint implements ui.Paint {
// TODO(ferhat): see https://github.com/flutter/flutter/issues/33605 // TODO(ferhat): see https://github.com/flutter/flutter/issues/33605
@override @override
double strokeMiterLimit = 0; double strokeMiterLimit = 4.0;
// TODO(ferhat): Implement ImageFilter, flutter/flutter#35156. // TODO(ferhat): Implement ImageFilter, flutter/flutter#35156.
@override @override

View File

@ -54,16 +54,25 @@ void testMain() {
setUpCanvasKitTest(withImplicitView: true); setUpCanvasKitTest(withImplicitView: true);
group('ImageFilters', () { group('ImageFilters', () {
test('can be constructed', () { {
final CkImageFilter imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10, tileMode: ui.TileMode.clamp); final testFilters = createImageFilters();
expect(imageFilter, isA<CkImageFilter>()); for (final imageFilter in testFilters) {
SkImageFilter? skFilter; test('${imageFilter.runtimeType}.withSkImageFilter creates temp SkImageFilter', () {
imageFilter.imageFilter((SkImageFilter value) { expect(imageFilter, isA<CkImageFilter>());
skFilter = value; SkImageFilter? skFilter;
}); imageFilter.withSkImageFilter((value) {
expect(skFilter, isNotNull); expect(value.isDeleted(), isFalse);
}); skFilter = value;
});
expect(skFilter, isNotNull);
expect(
reason: 'Because the SkImageFilter instance is temporary',
skFilter!.isDeleted(),
isTrue,
);
});
}
}
test('== operator', () { test('== operator', () {
final List<ui.ImageFilter> filters1 = <ui.ImageFilter>[ final List<ui.ImageFilter> filters1 = <ui.ImageFilter>[

View File

@ -7,7 +7,6 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart'; import 'package:ui/src/engine.dart';
import '../common/matchers.dart';
import 'common.dart'; import 'common.dart';
void main() { void main() {
@ -18,17 +17,11 @@ void testMain() {
group('CkPaint', () { group('CkPaint', () {
setUpCanvasKitTest(); setUpCanvasKitTest();
test('lifecycle', () { test('toSkPaint', () {
final CkPaint paint = CkPaint(); final paint = CkPaint();
expect(paint.skiaObject, isNotNull); final skPaint = paint.toSkPaint();
expect(paint.debugRef.isDisposed, isFalse); expect(skPaint, isNotNull);
paint.dispose(); skPaint.delete();
expect(paint.debugRef.isDisposed, isTrue);
expect(
reason: 'Cannot dispose more than once',
() => paint.dispose(),
throwsA(isAssertionError),
);
}); });
}); });
} }

View File

@ -18,6 +18,24 @@ Future<void> testMain() async {
setUpTestViewDimensions: false, setUpTestViewDimensions: false,
); );
test('default field values are as documented on api.flutter.dev', () {
final paint = ui.Paint();
expect(paint.blendMode, ui.BlendMode.srcOver);
expect(paint.color, const ui.Color(0xFF000000));
expect(paint.colorFilter, null);
expect(paint.filterQuality, ui.FilterQuality.none);
expect(paint.imageFilter, null);
expect(paint.invertColors, false);
expect(paint.isAntiAlias, true);
expect(paint.maskFilter, null);
expect(paint.shader, null);
expect(paint.strokeCap, ui.StrokeCap.butt);
expect(paint.strokeJoin, ui.StrokeJoin.miter);
expect(paint.strokeMiterLimit, 4.0);
expect(paint.strokeWidth, 0.0);
expect(paint.style, ui.PaintingStyle.fill);
});
test('toString()', () { test('toString()', () {
final ui.Paint paint = ui.Paint(); final ui.Paint paint = ui.Paint();
paint.blendMode = ui.BlendMode.darken; paint.blendMode = ui.BlendMode.darken;