[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.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();
paint
..blendMode = other.blendMode

View File

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

View File

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

View File

@ -71,7 +71,7 @@ abstract class CkColorFilter implements CkManagedSkImageFilterConvertible {
SkColorFilter _initRawColorFilter();
@override
void imageFilter(SkImageFilterBorrow borrow) {
void withSkImageFilter(SkImageFilterBorrow borrow) {
// Since ColorFilter has a const constructor it cannot store dynamically
// 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

View File

@ -300,7 +300,6 @@ CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) {
ui.Rect.fromLTWH(0, 0, targetWidth!.toDouble(), targetHeight!.toDouble()),
paint,
);
paint.dispose();
final CkPicture picture = recorder.endRecording();
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 'canvaskit_api.dart';
import 'color_filter.dart';
import 'native_memory.dart';
typedef SkImageFilterBorrow = void Function(SkImageFilter);
@ -22,7 +21,12 @@ typedef SkImageFilterBorrow = void Function(SkImageFilter);
///
/// Currently implemented by [CkImageFilter] and [CkColorFilter].
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;
}
@ -56,22 +60,15 @@ abstract class CkImageFilter implements CkManagedSkImageFilterConvertible {
}
class CkColorFilterImageFilter extends CkImageFilter {
CkColorFilterImageFilter({required this.colorFilter}) : super._() {
final SkImageFilter skImageFilter = colorFilter.initRawImageFilter();
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.color');
}
CkColorFilterImageFilter({required this.colorFilter}) : super._();
final CkColorFilter colorFilter;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
}
void dispose() {
_ref.dispose();
void withSkImageFilter(SkImageFilterBorrow borrow) {
final skImageFilter = colorFilter.initRawImageFilter();
borrow(skImageFilter);
skImageFilter.delete();
}
@override
@ -93,7 +90,14 @@ class CkColorFilterImageFilter extends CkImageFilter {
class _CkBlurImageFilter extends CkImageFilter {
_CkBlurImageFilter(
{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
/// effect of applying no filter
final SkImageFilter skImageFilter;
@ -110,18 +114,9 @@ class _CkBlurImageFilter extends CkImageFilter {
null,
);
}
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.blur');
}
final double sigmaX;
final double sigmaY;
final ui.TileMode tileMode;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
borrow(skImageFilter);
skImageFilter.delete();
}
@override
@ -149,25 +144,22 @@ class _CkMatrixImageFilter extends CkImageFilter {
{required Float64List matrix, required this.filterQuality})
: matrix = Float64List.fromList(matrix),
_transform = Matrix4.fromFloat32List(toMatrix32(matrix)),
super._() {
final SkImageFilter skImageFilter =
canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat64(matrix),
toSkFilterOptions(filterQuality),
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.matrix');
}
super._();
final Float64List matrix;
final ui.FilterQuality filterQuality;
final Matrix4 _transform;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
void withSkImageFilter(SkImageFilterBorrow borrow) {
final skImageFilter =
canvasKit.ImageFilter.MakeMatrixTransform(
toSkMatrixFromFloat64(matrix),
toSkFilterOptions(filterQuality),
null,
);
borrow(skImageFilter);
skImageFilter.delete();
}
@override
@ -192,23 +184,20 @@ class _CkMatrixImageFilter extends CkImageFilter {
class _CkDilateImageFilter extends CkImageFilter {
_CkDilateImageFilter({required this.radiusX, required this.radiusY})
: super._() {
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeDilate(
radiusX,
radiusY,
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.dilate');
}
: super._();
final double radiusX;
final double radiusY;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
void withSkImageFilter(SkImageFilterBorrow borrow) {
final skImageFilter = canvasKit.ImageFilter.MakeDilate(
radiusX,
radiusY,
null,
);
borrow(skImageFilter);
skImageFilter.delete();
}
@override
@ -232,23 +221,20 @@ class _CkDilateImageFilter extends CkImageFilter {
class _CkErodeImageFilter extends CkImageFilter {
_CkErodeImageFilter({required this.radiusX, required this.radiusY})
: super._() {
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeErode(
radiusX,
radiusY,
null,
);
_ref = UniqueRef<SkImageFilter>(this, skImageFilter, 'ImageFilter.erode');
}
: super._();
final double radiusX;
final double radiusY;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
void withSkImageFilter(SkImageFilterBorrow borrow) {
final skImageFilter = canvasKit.ImageFilter.MakeErode(
radiusX,
radiusY,
null,
);
borrow(skImageFilter);
skImageFilter.delete();
}
@override
@ -272,27 +258,23 @@ class _CkErodeImageFilter extends CkImageFilter {
class _CkComposeImageFilter extends CkImageFilter {
_CkComposeImageFilter({required this.outer, required this.inner})
: super._() {
outer.imageFilter((SkImageFilter outerFilter) {
inner.imageFilter((SkImageFilter innerFilter) {
final SkImageFilter skImageFilter = canvasKit.ImageFilter.MakeCompose(
outerFilter,
innerFilter,
);
_ref = UniqueRef<SkImageFilter>(
this, skImageFilter, 'ImageFilter.compose');
});
});
}
: super._();
final CkImageFilter outer;
final CkImageFilter inner;
late final UniqueRef<SkImageFilter> _ref;
@override
void imageFilter(SkImageFilterBorrow borrow) {
borrow(_ref.nativeObject);
void withSkImageFilter(SkImageFilterBorrow borrow) {
outer.withSkImageFilter((skOuter) {
inner.withSkImageFilter((skInner) {
final skImageFilter = canvasKit.ImageFilter.MakeCompose(
skOuter,
skInner,
);
borrow(skImageFilter);
skImageFilter.delete();
});
});
}
@override

View File

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

View File

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

View File

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

View File

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

View File

@ -54,16 +54,25 @@ void testMain() {
setUpCanvasKitTest(withImplicitView: true);
group('ImageFilters', () {
test('can be constructed', () {
final CkImageFilter imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10, tileMode: ui.TileMode.clamp);
expect(imageFilter, isA<CkImageFilter>());
SkImageFilter? skFilter;
imageFilter.imageFilter((SkImageFilter value) {
skFilter = value;
});
expect(skFilter, isNotNull);
});
{
final testFilters = createImageFilters();
for (final imageFilter in testFilters) {
test('${imageFilter.runtimeType}.withSkImageFilter creates temp SkImageFilter', () {
expect(imageFilter, isA<CkImageFilter>());
SkImageFilter? skFilter;
imageFilter.withSkImageFilter((value) {
expect(value.isDeleted(), isFalse);
skFilter = value;
});
expect(skFilter, isNotNull);
expect(
reason: 'Because the SkImageFilter instance is temporary',
skFilter!.isDeleted(),
isTrue,
);
});
}
}
test('== operator', () {
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 '../common/matchers.dart';
import 'common.dart';
void main() {
@ -18,17 +17,11 @@ void testMain() {
group('CkPaint', () {
setUpCanvasKitTest();
test('lifecycle', () {
final CkPaint paint = CkPaint();
expect(paint.skiaObject, isNotNull);
expect(paint.debugRef.isDisposed, isFalse);
paint.dispose();
expect(paint.debugRef.isDisposed, isTrue);
expect(
reason: 'Cannot dispose more than once',
() => paint.dispose(),
throwsA(isAssertionError),
);
test('toSkPaint', () {
final paint = CkPaint();
final skPaint = paint.toSkPaint();
expect(skPaint, isNotNull);
skPaint.delete();
});
});
}

View File

@ -18,6 +18,24 @@ Future<void> testMain() async {
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()', () {
final ui.Paint paint = ui.Paint();
paint.blendMode = ui.BlendMode.darken;