diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 74af083d28..128550b2b8 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1891,7 +1891,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart + ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart + ../../../flutter/LICENSE @@ -4487,7 +4486,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index 8c6b9fcef6..2af3a1719b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -50,7 +50,6 @@ export 'engine/canvaskit/raster_cache.dart'; export 'engine/canvaskit/rasterizer.dart'; export 'engine/canvaskit/renderer.dart'; export 'engine/canvaskit/shader.dart'; -export 'engine/canvaskit/skia_object_cache.dart'; export 'engine/canvaskit/surface.dart'; export 'engine/canvaskit/surface_factory.dart'; export 'engine/canvaskit/text.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index b458795ad8..6e4fa96e54 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -333,855 +333,4 @@ class CkCanvas { } return matrix4; } - - CkPictureSnapshot? get pictureSnapshot => null; -} - -class RecordingCkCanvas extends CkCanvas { - RecordingCkCanvas(super.skCanvas, ui.Rect bounds) - : pictureSnapshot = CkPictureSnapshot(bounds); - - @override - final CkPictureSnapshot pictureSnapshot; - - void _addCommand(CkPaintCommand command) { - pictureSnapshot._commands.add(command); - } - - @override - void clear(ui.Color color) { - super.clear(color); - _addCommand(CkClearCommand(color)); - } - - @override - void clipPath(CkPath path, bool doAntiAlias) { - super.clipPath(path, doAntiAlias); - _addCommand(CkClipPathCommand(path, doAntiAlias)); - } - - @override - void clipRRect(ui.RRect rrect, bool doAntiAlias) { - super.clipRRect(rrect, doAntiAlias); - _addCommand(CkClipRRectCommand(rrect, doAntiAlias)); - } - - @override - void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) { - super.clipRect(rect, clipOp, doAntiAlias); - _addCommand(CkClipRectCommand(rect, clipOp, doAntiAlias)); - } - - @override - void drawArc( - ui.Rect oval, - double startAngle, - double sweepAngle, - bool useCenter, - CkPaint paint, - ) { - super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); - _addCommand( - CkDrawArcCommand(oval, startAngle, sweepAngle, useCenter, paint)); - } - - @override - void drawAtlasRaw( - CkPaint paint, - CkImage atlas, - Float32List rstTransforms, - Float32List rects, - Uint32List? colors, - ui.BlendMode blendMode, - ) { - super.drawAtlasRaw(paint, atlas, rstTransforms, rects, colors, blendMode); - _addCommand(CkDrawAtlasCommand( - paint, atlas, rstTransforms, rects, colors, blendMode)); - } - - @override - void drawCircle(ui.Offset c, double radius, CkPaint paint) { - super.drawCircle(c, radius, paint); - _addCommand(CkDrawCircleCommand(c, radius, paint)); - } - - @override - void drawColor(ui.Color color, ui.BlendMode blendMode) { - super.drawColor(color, blendMode); - _addCommand(CkDrawColorCommand(color, blendMode)); - } - - @override - void drawDRRect(ui.RRect outer, ui.RRect inner, CkPaint paint) { - super.drawDRRect(outer, inner, paint); - _addCommand(CkDrawDRRectCommand(outer, inner, paint)); - } - - @override - void drawImage(CkImage image, ui.Offset offset, CkPaint paint) { - super.drawImage(image, offset, paint); - _addCommand(CkDrawImageCommand(image, offset, paint)); - } - - @override - void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) { - super.drawImageRect(image, src, dst, paint); - _addCommand(CkDrawImageRectCommand(image, src, dst, paint)); - } - - @override - void drawImageNine( - CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) { - super.drawImageNine(image, center, dst, paint); - _addCommand(CkDrawImageNineCommand(image, center, dst, paint)); - } - - @override - void drawLine(ui.Offset p1, ui.Offset p2, CkPaint paint) { - super.drawLine(p1, p2, paint); - _addCommand(CkDrawLineCommand(p1, p2, paint)); - } - - @override - void drawOval(ui.Rect rect, CkPaint paint) { - super.drawOval(rect, paint); - _addCommand(CkDrawOvalCommand(rect, paint)); - } - - @override - void drawPaint(CkPaint paint) { - super.drawPaint(paint); - _addCommand(CkDrawPaintCommand(paint)); - } - - @override - void drawParagraph(CkParagraph paragraph, ui.Offset offset) { - super.drawParagraph(paragraph, offset); - _addCommand(CkDrawParagraphCommand(paragraph, offset)); - } - - @override - void drawPath(CkPath path, CkPaint paint) { - super.drawPath(path, paint); - _addCommand(CkDrawPathCommand(path, paint)); - } - - @override - void drawPicture(CkPicture picture) { - super.drawPicture(picture); - _addCommand(CkDrawPictureCommand(picture)); - } - - @override - void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { - super.drawPoints(paint, pointMode, points); - _addCommand(CkDrawPointsCommand(pointMode, points, paint)); - } - - @override - void drawRRect(ui.RRect rrect, CkPaint paint) { - super.drawRRect(rrect, paint); - _addCommand(CkDrawRRectCommand(rrect, paint)); - } - - @override - void drawRect(ui.Rect rect, CkPaint paint) { - super.drawRect(rect, paint); - _addCommand(CkDrawRectCommand(rect, paint)); - } - - @override - void drawShadow( - CkPath path, ui.Color color, double elevation, bool transparentOccluder) { - super.drawShadow(path, color, elevation, transparentOccluder); - _addCommand( - CkDrawShadowCommand(path, color, elevation, transparentOccluder)); - } - - @override - void drawVertices( - CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) { - super.drawVertices(vertices, blendMode, paint); - _addCommand(CkDrawVerticesCommand(vertices, blendMode, paint)); - } - - @override - void restore() { - super.restore(); - _addCommand(const CkRestoreCommand()); - } - - @override - void restoreToCount(int count) { - super.restoreToCount(count); - _addCommand(CkRestoreToCountCommand(count)); - } - - @override - void rotate(double radians) { - super.rotate(radians); - _addCommand(CkRotateCommand(radians)); - } - - @override - int save() { - _addCommand(const CkSaveCommand()); - return super.save(); - } - - @override - void saveLayer(ui.Rect bounds, CkPaint? paint) { - super.saveLayer(bounds, paint); - _addCommand(CkSaveLayerCommand(bounds, paint)); - } - - @override - void saveLayerWithoutBounds(CkPaint? paint) { - super.saveLayerWithoutBounds(paint); - _addCommand(CkSaveLayerWithoutBoundsCommand(paint)); - } - - @override - void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter, - [CkPaint? paint]) { - super.saveLayerWithFilter(bounds, filter, paint); - _addCommand(CkSaveLayerWithFilterCommand(bounds, filter, paint)); - } - - @override - void scale(double sx, double sy) { - super.scale(sx, sy); - _addCommand(CkScaleCommand(sx, sy)); - } - - @override - void skew(double sx, double sy) { - super.skew(sx, sy); - _addCommand(CkSkewCommand(sx, sy)); - } - - @override - void transform(Float32List matrix4) { - super.transform(matrix4); - _addCommand(CkTransformCommand(matrix4)); - } - - @override - void translate(double dx, double dy) { - super.translate(dx, dy); - _addCommand(CkTranslateCommand(dx, dy)); - } -} - -class CkPictureSnapshot { - CkPictureSnapshot(this._bounds); - - final ui.Rect _bounds; - final List _commands = []; - - SkPicture toPicture() { - final SkPictureRecorder recorder = SkPictureRecorder(); - final Float32List skRect = toSkRect(_bounds); - final SkCanvas skCanvas = recorder.beginRecording(skRect); - for (final CkPaintCommand command in _commands) { - command.apply(skCanvas); - } - final SkPicture skPicture = recorder.finishRecordingAsPicture(); - recorder.delete(); - return skPicture; - } - - void dispose() { - for (final CkPaintCommand command in _commands) { - command.dispose(); - } - } -} - -/// A paint command recorded by [RecordingCkCanvas]. -/// -/// # Special rules when drawing images -/// -/// A command painting an image must clone the original image to bump the ref -/// count. Otherwise when the framework decides it doesn't need the image any -/// more it will bump the ref count down and delete the underlying Skia object, -/// leaving the picture that recorded this paint command with a dangling -/// pointer. If we attempt to resurrect the picture we'll hit a use-after-free -/// error. The command must call [CkImage.dispose] in its [dispose] -/// implementation. -abstract class CkPaintCommand { - const CkPaintCommand(); - - /// Applies the command onto the [canvas]. - void apply(SkCanvas canvas); - - /// Frees resources associated with the command. - void dispose() {} -} - -class CkClearCommand extends CkPaintCommand { - const CkClearCommand(this.color); - - final ui.Color color; - - @override - void apply(SkCanvas canvas) { - canvas.clear(toSharedSkColor1(color)); - } -} - -class CkSaveCommand extends CkPaintCommand { - const CkSaveCommand(); - - @override - void apply(SkCanvas canvas) { - canvas.save(); - } -} - -class CkRestoreCommand extends CkPaintCommand { - const CkRestoreCommand(); - - @override - void apply(SkCanvas canvas) { - canvas.restore(); - } -} - -class CkRestoreToCountCommand extends CkPaintCommand { - const CkRestoreToCountCommand(this.count); - - final int count; - - @override - void apply(SkCanvas canvas) { - canvas.restoreToCount(count.toDouble()); - } -} - -class CkTranslateCommand extends CkPaintCommand { - CkTranslateCommand(this.dx, this.dy); - - final double dx; - final double dy; - - @override - void apply(SkCanvas canvas) { - canvas.translate(dx, dy); - } -} - -class CkScaleCommand extends CkPaintCommand { - CkScaleCommand(this.sx, this.sy); - - final double sx; - final double sy; - - @override - void apply(SkCanvas canvas) { - canvas.scale(sx, sy); - } -} - -class CkRotateCommand extends CkPaintCommand { - CkRotateCommand(this.radians); - - final double radians; - - @override - void apply(SkCanvas canvas) { - canvas.rotate(radians * 180.0 / math.pi, 0.0, 0.0); - } -} - -class CkTransformCommand extends CkPaintCommand { - CkTransformCommand(this.matrix4); - - final Float32List matrix4; - - @override - void apply(SkCanvas canvas) { - canvas.concat(toSkM44FromFloat32(matrix4)); - } -} - -class CkSkewCommand extends CkPaintCommand { - CkSkewCommand(this.sx, this.sy); - - final double sx; - final double sy; - - @override - void apply(SkCanvas canvas) { - canvas.skew(sx, sy); - } -} - -class CkClipRectCommand extends CkPaintCommand { - CkClipRectCommand(this.rect, this.clipOp, this.doAntiAlias); - - final ui.Rect rect; - final ui.ClipOp clipOp; - final bool doAntiAlias; - - @override - void apply(SkCanvas canvas) { - canvas.clipRect( - toSkRect(rect), - toSkClipOp(clipOp), - doAntiAlias, - ); - } -} - -class CkDrawArcCommand extends CkPaintCommand { - CkDrawArcCommand( - this.oval, this.startAngle, this.sweepAngle, this.useCenter, this.paint); - - final ui.Rect oval; - final double startAngle; - final double sweepAngle; - final bool useCenter; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - const double toDegrees = 180 / math.pi; - canvas.drawArc( - toSkRect(oval), - startAngle * toDegrees, - sweepAngle * toDegrees, - useCenter, - paint.skiaObject, - ); - } -} - -class CkDrawAtlasCommand extends CkPaintCommand { - CkDrawAtlasCommand(this.paint, this.atlas, this.rstTransforms, this.rects, - this.colors, this.blendMode); - - final CkPaint paint; - final CkImage atlas; - final Float32List rstTransforms; - final Float32List rects; - final Uint32List? colors; - final ui.BlendMode blendMode; - - @override - void apply(SkCanvas canvas) { - canvas.drawAtlas( - atlas.skImage, - rects, - rstTransforms, - paint.skiaObject, - toSkBlendMode(blendMode), - colors, - ); - } -} - -class CkClipRRectCommand extends CkPaintCommand { - CkClipRRectCommand(this.rrect, this.doAntiAlias); - - final ui.RRect rrect; - final bool doAntiAlias; - - @override - void apply(SkCanvas canvas) { - canvas.clipRRect( - toSkRRect(rrect), - _clipOpIntersect, - doAntiAlias, - ); - } -} - -class CkClipPathCommand extends CkPaintCommand { - CkClipPathCommand(this.path, this.doAntiAlias); - - final CkPath path; - final bool doAntiAlias; - - @override - void apply(SkCanvas canvas) { - canvas.clipPath( - path.skiaObject, - _clipOpIntersect, - doAntiAlias, - ); - } -} - -class CkDrawColorCommand extends CkPaintCommand { - CkDrawColorCommand(this.color, this.blendMode); - - final ui.Color color; - final ui.BlendMode blendMode; - - @override - void apply(SkCanvas canvas) { - canvas.drawColorInt( - color.value.toDouble(), - toSkBlendMode(blendMode), - ); - } -} - -class CkDrawLineCommand extends CkPaintCommand { - CkDrawLineCommand(this.p1, this.p2, this.paint); - - final ui.Offset p1; - final ui.Offset p2; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawLine( - p1.dx, - p1.dy, - p2.dx, - p2.dy, - paint.skiaObject, - ); - } -} - -class CkDrawPaintCommand extends CkPaintCommand { - CkDrawPaintCommand(this.paint); - - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawPaint(paint.skiaObject); - } -} - -class CkDrawVerticesCommand extends CkPaintCommand { - CkDrawVerticesCommand(this.vertices, this.blendMode, this.paint); - - final CkVertices vertices; - final ui.BlendMode blendMode; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawVertices( - vertices.skiaObject, - toSkBlendMode(blendMode), - paint.skiaObject, - ); - } -} - -class CkDrawPointsCommand extends CkPaintCommand { - CkDrawPointsCommand(this.pointMode, this.points, this.paint); - - final Float32List points; - final ui.PointMode pointMode; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawPoints( - toSkPointMode(pointMode), - points, - paint.skiaObject, - ); - } -} - -class CkDrawRectCommand extends CkPaintCommand { - CkDrawRectCommand(this.rect, this.paint); - - final ui.Rect rect; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawRect(toSkRect(rect), paint.skiaObject); - } -} - -class CkDrawRRectCommand extends CkPaintCommand { - CkDrawRRectCommand(this.rrect, this.paint); - - final ui.RRect rrect; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawRRect( - toSkRRect(rrect), - paint.skiaObject, - ); - } -} - -class CkDrawDRRectCommand extends CkPaintCommand { - CkDrawDRRectCommand(this.outer, this.inner, this.paint); - - final ui.RRect outer; - final ui.RRect inner; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawDRRect( - toSkRRect(outer), - toSkRRect(inner), - paint.skiaObject, - ); - } -} - -class CkDrawOvalCommand extends CkPaintCommand { - CkDrawOvalCommand(this.rect, this.paint); - - final ui.Rect rect; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawOval( - toSkRect(rect), - paint.skiaObject, - ); - } -} - -class CkDrawCircleCommand extends CkPaintCommand { - CkDrawCircleCommand(this.c, this.radius, this.paint); - - final ui.Offset c; - final double radius; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawCircle( - c.dx, - c.dy, - radius, - paint.skiaObject, - ); - } -} - -class CkDrawPathCommand extends CkPaintCommand { - CkDrawPathCommand(this.path, this.paint); - - final CkPath path; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawPath(path.skiaObject, paint.skiaObject); - } -} - -class CkDrawShadowCommand extends CkPaintCommand { - CkDrawShadowCommand( - this.path, this.color, this.elevation, this.transparentOccluder); - - final CkPath path; - final ui.Color color; - final double elevation; - final bool transparentOccluder; - - @override - void apply(SkCanvas canvas) { - drawSkShadow(canvas, path, color, elevation, transparentOccluder, - ui.window.devicePixelRatio); - } -} - -class CkDrawImageCommand extends CkPaintCommand { - CkDrawImageCommand(CkImage ckImage, this.offset, this.paint) - : image = ckImage.clone(); - - final CkImage image; - final ui.Offset offset; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - final ui.FilterQuality filterQuality = paint.filterQuality; - if (filterQuality == ui.FilterQuality.high) { - canvas.drawImageCubic( - image.skImage, - offset.dx, - offset.dy, - CkCanvas._kMitchellNetravali_B, - CkCanvas._kMitchellNetravali_C, - paint.skiaObject, - ); - } else { - canvas.drawImageOptions( - image.skImage, - offset.dx, - offset.dy, - toSkFilterMode(filterQuality), - toSkMipmapMode(filterQuality), - paint.skiaObject, - ); - } - } - - @override - void dispose() { - image.dispose(); - } -} - -class CkDrawImageRectCommand extends CkPaintCommand { - CkDrawImageRectCommand(CkImage ckImage, this.src, this.dst, this.paint) - : image = ckImage.clone(); - - final CkImage image; - final ui.Rect src; - final ui.Rect dst; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - final ui.FilterQuality filterQuality = paint.filterQuality; - if (filterQuality == ui.FilterQuality.high) { - canvas.drawImageRectCubic( - image.skImage, - toSkRect(src), - toSkRect(dst), - CkCanvas._kMitchellNetravali_B, - CkCanvas._kMitchellNetravali_C, - paint.skiaObject, - ); - } else { - canvas.drawImageRectOptions( - image.skImage, - toSkRect(src), - toSkRect(dst), - toSkFilterMode(filterQuality), - toSkMipmapMode(filterQuality), - paint.skiaObject, - ); - } - } - - @override - void dispose() { - image.dispose(); - } -} - -class CkDrawImageNineCommand extends CkPaintCommand { - CkDrawImageNineCommand(CkImage ckImage, this.center, this.dst, this.paint) - : image = ckImage.clone(); - - final CkImage image; - final ui.Rect center; - final ui.Rect dst; - final CkPaint paint; - - @override - void apply(SkCanvas canvas) { - canvas.drawImageNine( - image.skImage, - toSkRect(center), - toSkRect(dst), - toSkFilterMode(paint.filterQuality), - paint.skiaObject, - ); - } - - @override - void dispose() { - image.dispose(); - } -} - -class CkDrawParagraphCommand extends CkPaintCommand { - CkDrawParagraphCommand(this.paragraph, this.offset); - - final CkParagraph paragraph; - final ui.Offset offset; - - @override - void apply(SkCanvas canvas) { - canvas.drawParagraph( - paragraph.skiaObject, - offset.dx, - offset.dy, - ); - } -} - -class CkDrawPictureCommand extends CkPaintCommand { - CkDrawPictureCommand(this.picture); - - final CkPicture picture; - - @override - void apply(SkCanvas canvas) { - canvas.drawPicture(picture.skiaObject); - } -} - -class CkSaveLayerCommand extends CkPaintCommand { - CkSaveLayerCommand(this.bounds, this.paint); - - final ui.Rect bounds; - final CkPaint? paint; - - @override - void apply(SkCanvas canvas) { - canvas.saveLayer( - paint?.skiaObject, - toSkRect(bounds), - null, - null, - ); - } -} - -class CkSaveLayerWithoutBoundsCommand extends CkPaintCommand { - CkSaveLayerWithoutBoundsCommand(this.paint); - - final CkPaint? paint; - - @override - void apply(SkCanvas canvas) { - canvas.saveLayer( - paint?.skiaObject, - null, - null, - null, - ); - } -} - -class CkSaveLayerWithFilterCommand extends CkPaintCommand { - CkSaveLayerWithFilterCommand(this.bounds, this.filter, this.paint); - - final ui.Rect bounds; - final ui.ImageFilter filter; - final CkPaint? paint; - - @override - void apply(SkCanvas canvas) { - final CkManagedSkImageFilterConvertible convertible; - if (filter is ui.ColorFilter) { - convertible = createCkColorFilter(filter as EngineColorFilter)!; - } else { - convertible = filter as CkManagedSkImageFilterConvertible; - } - convertible.imageFilter((SkImageFilter filter) { - canvas.saveLayer( - paint?.skiaObject, - toSkRect(bounds), - filter, - 0, - ); - }); - } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index faacd1dd88..912fc41759 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -23,7 +23,6 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../configuration.dart'; import '../dom.dart'; -import '../profiler.dart'; import 'renderer.dart'; /// Entrypoint into the CanvasKit API. @@ -3350,172 +3349,6 @@ extension SkTypefaceFactoryExtension on SkTypefaceFactory { _MakeFreeTypeFaceFromData(fontData.toJS); } -/// Collects Skia objects that are no longer necessary. -abstract class Collector { - /// The production collector implementation. - static final Collector _productionInstance = ProductionCollector(); - - /// The collector implementation currently in use. - static Collector get instance => _instance; - static Collector _instance = _productionInstance; - - /// In tests overrides the collector implementation. - static void debugOverrideCollector(Collector override) { - _instance = override; - } - - /// In tests restores the collector to the production implementation. - static void debugRestoreCollector() { - _instance = _productionInstance; - } - - /// Registers a [deletable] for collection when the [wrapper] object is - /// garbage collected. - /// - /// The [debugLabel] is used to track the origin of the deletable. - void register(Object wrapper, SkDeletable deletable); - - /// Deletes the [deletable]. - /// - /// The exact timing of the deletion is implementation-specific. For example, - /// a production implementation may want to batch deletables and schedule a - /// timer to collect them instead of deleting right away. - /// - /// A test implementation may want a collection strategy that's less efficient - /// but more predictable. - void collect(SkDeletable deletable); -} - -/// Uses the browser's real `FinalizationRegistry` to collect objects. -/// -/// Uses timers to delete objects in batches and outside the animation frame. -class ProductionCollector implements Collector { - ProductionCollector() { - _skObjectFinalizationRegistry = - createSkObjectFinalizationRegistry((SkDeletable deletable) { - // This is called when GC decides to collect the wrapper object and - // notify us, which may happen after the object is already deleted - // explicitly, e.g. when its ref count drops to zero. When that happens - // skip collection of this object. - if (!deletable.isDeleted()) { - collect(deletable); - } - }.toJS); - } - - late final SkObjectFinalizationRegistry _skObjectFinalizationRegistry; - List _skiaObjectCollectionQueue = []; - Timer? _skiaObjectCollectionTimer; - - @override - void register(Object wrapper, SkDeletable deletable) { - if (Instrumentation.enabled) { - Instrumentation.instance.incrementCounter( - '${deletable.constructor.name} registered', - ); - } - _skObjectFinalizationRegistry.register(wrapper, deletable); - } - - /// Schedules a Skia object for deletion in an asap timer. - /// - /// A timer is used for the following reasons: - /// - /// - Deleting the object immediately may lead to dangling pointer as the Skia - /// object may still be used by a function in the current frame. For example, - /// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to - /// the engine, and the `CkPaint` dropped immediately. Because GC can kick in - /// any time, including in the middle of the event, we may delete `SkPaint` - /// prematurely. - /// - A microtask, while solves the problem above, would prevent the event from - /// yielding to the graphics system to render the frame on the screen if there - /// is a large number of objects to delete, causing jank. - /// - /// Because scheduling a timer is expensive, the timer is shared by all objects - /// deleted this frame. No timer is created if no objects were scheduled for - /// deletion. - @override - void collect(SkDeletable deletable) { - assert( - !deletable.isDeleted(), - 'Attempted to delete an already deleted Skia object.', - ); - _skiaObjectCollectionQueue.add(deletable); - - _skiaObjectCollectionTimer ??= Timer(Duration.zero, () { - // Null out the timer so we can schedule a new one next time objects are - // scheduled for deletion. - _skiaObjectCollectionTimer = null; - collectSkiaObjectsNow(); - }); - } - - /// Deletes all Skia objects pending deletion synchronously. - /// - /// After calling this method [_skiaObjectCollectionQueue] is empty. - /// - /// Throws a [SkiaObjectCollectionError] if CanvasKit fails to delete at least - /// one object. The error is populated with information about the first failed - /// object. Upon an error the collection continues and the collection queue is - /// emptied out to prevent memory leaks. This may happen, for example, when the - /// same object is deleted more than once. - void collectSkiaObjectsNow() { - domWindow.performance.mark('SkObject collection-start'); - final int length = _skiaObjectCollectionQueue.length; - dynamic firstError; - StackTrace? firstStackTrace; - for (int i = 0; i < length; i++) { - final SkDeletable deletable = _skiaObjectCollectionQueue[i]; - if (deletable.isDeleted()) { - // Some Skia objects are ref counted and are deleted before GC and/or - // the collection timer begins collecting them. So we have to check - // again if the objects is worth collecting. - continue; - } - if (Instrumentation.enabled) { - Instrumentation.instance.incrementCounter( - '${deletable.constructor.name} deleted', - ); - } - try { - deletable.delete(); - } catch (error, stackTrace) { - // Remember the error, but keep going. If for some reason CanvasKit fails - // to delete an object we still want to delete other objects and empty - // out the queue. Otherwise, the queue will never be flushed and keep - // accumulating objects, a.k.a. memory leak. - if (firstError == null) { - firstError = error; - firstStackTrace = stackTrace; - } - } - } - _skiaObjectCollectionQueue = []; - - domWindow.performance.mark('SkObject collection-end'); - domWindow.performance.measure('SkObject collection', - 'SkObject collection-start', 'SkObject collection-end'); - - // It's safe to throw the error here, now that we've processed the queue. - if (firstError != null) { - throw SkiaObjectCollectionError(firstError, firstStackTrace); - } - } -} - -/// Thrown by [ProductionCollector] when Skia object collection fails. -class SkiaObjectCollectionError implements Error { - SkiaObjectCollectionError(this.error, this.stackTrace); - - final dynamic error; - - @override - final StackTrace? stackTrace; - - @override - String toString() => 'SkiaObjectCollectionError: $error\n$stackTrace'; -} - /// Any Skia object that has a `delete` method. @JS() @anonymous @@ -3591,12 +3424,6 @@ external JSAny? get _finalizationRegistryConstructor; bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; -/// Sets the value of [browserSupportsFinalizationRegistry] to its true value. -void debugResetBrowserSupportsFinalizationRegistry() { - browserSupportsFinalizationRegistry = - _finalizationRegistryConstructor != null; -} - @JS() @staticInterop class SkData {} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart index a83fc2453b..efe7c544d9 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image_wasm_codecs.dart @@ -17,29 +17,28 @@ import 'package:ui/ui.dart' as ui; import '../util.dart'; import 'canvaskit_api.dart'; import 'image.dart'; -import 'skia_object_cache.dart'; +import 'native_memory.dart'; /// The CanvasKit implementation of [ui.Codec]. /// /// Wraps `SkAnimatedImage`. -class CkAnimatedImage extends ManagedSkiaObject - implements ui.Codec { +class CkAnimatedImage implements ui.Codec { /// Decodes an image from a list of encoded bytes. - CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight}); + CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight}) { + final SkAnimatedImage skAnimatedImage = createSkAnimatedImage(); + _ref = UniqueRef(this, skAnimatedImage, 'Codec'); + } + late final UniqueRef _ref; final String src; final Uint8List _bytes; int _frameCount = 0; int _repetitionCount = -1; - /// Current frame index. - int _currentFrameIndex = 0; - final int? targetWidth; final int? targetHeight; - @override - SkAnimatedImage createDefault() { + SkAnimatedImage createSkAnimatedImage() { SkAnimatedImage? animatedImage = canvasKit.MakeAnimatedImageFromEncoded(_bytes); if (animatedImage == null) { @@ -66,16 +65,6 @@ class CkAnimatedImage extends ManagedSkiaObject _frameCount = animatedImage.getFrameCount().toInt(); _repetitionCount = animatedImage.getRepetitionCount().toInt(); - // Normally CanvasKit initializes `SkAnimatedImage` to point to the first - // frame in the animation. However, if the Skia object has been deleted then - // resurrected, the framework/app may already have advanced to one of the - // subsequent frames. When that happens the value of _currentFrameIndex will - // be something other than zero, and we need to tell the decoder to skip - // over the previous frames to point to the current one. - for (int i = 0; i < _currentFrameIndex; i++) { - animatedImage.decodeNextFrame(); - } - return animatedImage; } @@ -92,17 +81,6 @@ class CkAnimatedImage extends ManagedSkiaObject return resizedAnimatedImage; } - @override - SkAnimatedImage resurrect() => createDefault(); - - @override - bool get isResurrectionExpensive => true; - - @override - void delete() { - rawSkiaObject?.delete(); - } - bool _disposed = false; bool get debugDisposed => _disposed; @@ -118,7 +96,7 @@ class CkAnimatedImage extends ManagedSkiaObject 'Cannot dispose a codec that has already been disposed.', ); _disposed = true; - delete(); + _ref.dispose(); } @override @@ -136,7 +114,7 @@ class CkAnimatedImage extends ManagedSkiaObject @override Future getNextFrame() { assert(_debugCheckIsNotDisposed()); - final SkAnimatedImage animatedImage = skiaObject; + final SkAnimatedImage animatedImage = _ref.nativeObject; // SkAnimatedImage comes pre-initialized to point to the current frame (by // default the first frame, and, with some special resurrection logic in @@ -152,7 +130,6 @@ class CkAnimatedImage extends ManagedSkiaObject ); animatedImage.decodeNextFrame(); - _currentFrameIndex = (_currentFrameIndex + 1) % _frameCount; return Future.value(currentFrame); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart index dd9ac780fc..b912c2e821 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart @@ -16,7 +16,6 @@ import 'image_filter.dart'; import 'mask_filter.dart'; import 'native_memory.dart'; import 'shader.dart'; -import 'skia_object_cache.dart'; /// The implementation of [ui.Paint] used by the CanvasKit backend. /// diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 936d5897ba..5fdc0ccb19 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -11,25 +11,20 @@ import '../util.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; import 'image.dart'; -import 'skia_object_cache.dart'; +import 'native_memory.dart'; import 'surface.dart'; import 'surface_factory.dart'; /// Implements [ui.Picture] on top of [SkPicture]. -/// -/// Unlike most other [ManagedSkiaObject] implementations, instances of this -/// class may have their Skia counterparts deleted before finalization registry -/// or [SkiaObjectCache] decide to delete it. -class CkPicture extends ManagedSkiaObject implements ui.Picture { - CkPicture(SkPicture super.picture, this.cullRect, this._snapshot) : - assert( - browserSupportsFinalizationRegistry && _snapshot == null || - _snapshot != null, - 'If the browser does not support FinalizationRegistry (WeakRef), then we must have a picture snapshot to be able to resurrect it.', - ); +class CkPicture implements ui.Picture { + CkPicture(SkPicture skPicture, this.cullRect) { + _ref = UniqueRef(this, skPicture, 'Picture'); + } + late final UniqueRef _ref; final ui.Rect? cullRect; - final CkPictureSnapshot? _snapshot; + + SkPicture get skiaObject => _ref.nativeObject; @override int get approximateBytesUsed => 0; @@ -86,11 +81,7 @@ class CkPicture extends ManagedSkiaObject implements ui.Picture { Instrumentation.instance.incrementCounter('Picture disposed'); } _isDisposed = true; - _snapshot?.dispose(); - - // Emulate what SkiaObjectCache does. - rawSkiaObject?.delete(); - rawSkiaObject = null; + _ref.dispose(); } @override @@ -123,31 +114,4 @@ class CkPicture extends ManagedSkiaObject implements ui.Picture { } return CkImage(rasterImage); } - - @override - bool get isResurrectionExpensive => true; - - @override - SkPicture createDefault() { - // The default object is supplied in the constructor. - throw StateError('Unreachable code'); - } - - @override - SkPicture resurrect() { - // If a picture has been explicitly disposed of, it can no longer be - // resurrected. An attempt to resurrect after the framework told the - // engine to dispose of the picture likely indicates a bug in the engine. - assert(debugCheckNotDisposed('Cannot resurrect picture.')); - return _snapshot!.toPicture(); - } - - @override - void delete() { - // This method may be called after [dispose], in which case there's nothing - // left to do. The Skia object is deleted permanently. - if (!_isDisposed) { - rawSkiaObject?.delete(); - } - } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart index 7a0d11e433..becc3996d0 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart @@ -20,9 +20,7 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPictureRecorder recorder = _skRecorder = SkPictureRecorder(); final Float32List skRect = toSkRect(bounds); final SkCanvas skCanvas = recorder.beginRecording(skRect); - return _recordingCanvas = browserSupportsFinalizationRegistry - ? CkCanvas(skCanvas) - : RecordingCkCanvas(skCanvas, bounds); + return _recordingCanvas = CkCanvas(skCanvas); } CkCanvas? get recordingCanvas => _recordingCanvas; @@ -38,8 +36,7 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPicture skPicture = recorder.finishRecordingAsPicture(); recorder.delete(); _skRecorder = null; - final CkPicture result = - CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot); + final CkPicture result = CkPicture(skPicture, _cullRect); // We invoke the handler here, not in the picture constructor, because we want // [result.approximateBytesUsed] to be available for the handler. ui.Picture.onCreate?.call(result); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart deleted file mode 100644 index 677397b953..0000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:collection'; - -import 'package:meta/meta.dart'; - -import '../../engine.dart' show Instrumentation; -import 'canvaskit_api.dart'; -import 'renderer.dart'; - -/// A cache of Skia objects whose memory Flutter manages. -/// -/// When using Skia, Flutter creates Skia objects which are allocated in -/// WASM memory and which must be explicitly deleted. In the case of Flutter -/// mobile, the Skia objects are wrapped by a C++ class which is destroyed -/// when the associated Dart object is garbage collected. -/// -/// On the web, we cannot tell when a Dart object is garbage collected, so -/// we must use other strategies to know when to delete a Skia object. Some -/// objects, like [ui.Paint], can safely delete their associated Skia object -/// because they can always recreate the Skia object from data stored in the -/// Dart object. Other objects, like [ui.Picture], can be serialized to a -/// JS-managed data structure when they are deleted so that when the associated -/// object is garbage collected, so is the serialized data. -class SkiaObjectCache { - SkiaObjectCache(this.maximumSize) - : _itemQueue = DoubleLinkedQueue>(), - _itemMap = , DoubleLinkedQueueEntry>>{}; - - final int maximumSize; - - /// A doubly linked list of the objects in the cache. - /// - /// This makes it fast to move a recently used object to the front. - final DoubleLinkedQueue> _itemQueue; - - /// A map of objects to their associated node in the [_itemQueue]. - /// - /// This makes it fast to find the node in the queue when we need to - /// move the object to the front of the queue. - final Map, DoubleLinkedQueueEntry>> _itemMap; - - /// The number of objects in the cache. - int get length => _itemQueue.length; - - /// Whether or not [object] is in the cache. - /// - /// This is only for testing. - @visibleForTesting - bool debugContains(SkiaObject object) { - return _itemMap.containsKey(object); - } - - /// Adds [object] to the cache. - /// - /// If adding [object] causes the total size of the cache to exceed - /// [maximumSize], then the least recently used half of the cache - /// will be deleted. - void add(SkiaObject object) { - _itemQueue.addFirst(object); - _itemMap[object] = _itemQueue.firstEntry()!; - - if (_itemQueue.length > maximumSize) { - SkiaObjects.markCacheForResize(this); - } - } - - /// Records that [object] was used in the most recent frame. - void markUsed(SkiaObject object) { - final DoubleLinkedQueueEntry> item = _itemMap[object]!; - item.remove(); - _itemQueue.addFirst(object); - _itemMap[object] = _itemQueue.firstEntry()!; - } - - /// Deletes the least recently used half of this cache. - void resize() { - final int itemsToDelete = maximumSize ~/ 2; - for (int i = 0; i < itemsToDelete; i++) { - final SkiaObject oldObject = _itemQueue.removeLast(); - _itemMap.remove(oldObject); - oldObject.delete(); - oldObject.didDelete(); - } - } -} - -/// An object backed by a JavaScript object mapped onto a Skia C++ object in the -/// WebAssembly heap. -/// -/// These objects are automatically deleted when no longer used. -abstract class SkiaObject { - /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. - T get skiaObject; - - /// Deletes the associated C++ object from the WebAssembly heap. - void delete(); - - /// Lifecycle method called immediately after calling [delete]. - /// - /// This method is used to - void didDelete(); -} - -/// A [SkiaObject] that manages the lifecycle of its C++ counterpart. -/// -/// In browsers that support weak references we use feedback from the garbage -/// collector to determine when it is safe to release the C++ object. -/// -/// In browsers that do not support weak references we pessimistically delete -/// the underlying C++ object before the Dart object is garbage-collected. -/// -/// If [isResurrectionExpensive] is false the object is deleted at the end of -/// the frame. If a deleted object is reused in a subsequent frame it is -/// resurrected by calling [resurrect]. This allows reusing the C++ objects -/// within the frame. -/// -/// If [isResurrectionExpensive] is true the object is put in a LRU cache. -/// Objects that are used least frequently are deleted from the cache when -/// the cache limit is reached. -/// -/// The lifecycle of a resurrectable C++ object is as follows: -/// -/// - Create: a managed object is created using a default instance that's -/// either supplied as a constructor argument, or obtained by calling -/// [createDefault]. The data in the new object is expected to contain -/// data matching Flutter's defaults (sometimes Skia defaults need to be -/// adjusted). -/// - Zero or more cycles of delete + resurrect: when a Dart object is reused -/// after its C++ object is deleted we create a new C++ object populated with -/// data from the current state of the Dart object. This is done using the -/// [resurrect] method. -/// - Final delete: if a Dart object is never reused, it is GC'd after its -/// underlying C++ object is deleted. This is implemented by [SkiaObjects]. -abstract class ManagedSkiaObject extends SkiaObject { - /// Creates a managed Skia object. - /// - /// If `instance` is null calls [createDefault] to create a Skia object to - /// manage. Otherwise, uses the provided instance. - /// - /// The provided instance must not be managed by another [ManagedSkiaObject], - /// as it may lead to undefined behavior. - ManagedSkiaObject([T? instance]) { - final T defaultObject = instance ?? createDefault(); - rawSkiaObject = defaultObject; - if (browserSupportsFinalizationRegistry) { - // If FinalizationRegistry is supported we will only ever need the - // default object, as we know precisely when to delete it. - Collector.instance.register(this, defaultObject as SkDeletable); - } else { - // If FinalizationRegistry is _not_ supported we may need to delete - // and resurrect the object multiple times before deleting it forever. - if (Instrumentation.enabled) { - Instrumentation.instance.incrementCounter( - '${(defaultObject as SkDeletable).constructor.name} created', - ); - } - if (isResurrectionExpensive) { - SkiaObjects.manageExpensive(this); - } else { - SkiaObjects.manageResurrectable(this); - } - } - } - - @override - T get skiaObject => rawSkiaObject ?? _doResurrect(); - - T _doResurrect() { - assert(!browserSupportsFinalizationRegistry); - final T skiaObject = resurrect(); - if (Instrumentation.enabled) { - Instrumentation.instance.incrementCounter( - '${(skiaObject as SkDeletable).constructor.name} resurrected', - ); - } - rawSkiaObject = skiaObject; - if (isResurrectionExpensive) { - SkiaObjects.manageExpensive(this); - } else { - SkiaObjects.manageResurrectable(this); - } - return skiaObject; - } - - @override - void didDelete() { - assert(!browserSupportsFinalizationRegistry); - - // Null indicates that the object has been manually disposed of. This - // happens for objects with manual lifecycles, such as Picture. - if (rawSkiaObject == null) { - return; - } - - if (Instrumentation.enabled) { - Instrumentation.instance.incrementCounter( - '${(rawSkiaObject! as SkDeletable).constructor.name} deleted', - ); - } - rawSkiaObject = null; - } - - /// Returns the current skia object as is without attempting to - /// resurrect it. - /// - /// If the returned value is `null`, the corresponding C++ object has - /// been deleted. - /// - /// Use this field instead of the [skiaObject] getter when implementing - /// the [delete] method. - T? rawSkiaObject; - - /// Instantiates a new Skia-backed JavaScript object containing default - /// values. - /// - /// The object is expected to represent Flutter's defaults. If Skia uses - /// different defaults from those used by Flutter, this method is expected - /// initialize the object to Flutter's defaults. - T createDefault(); - - /// Creates a new Skia-backed JavaScript object containing data representing - /// the current state of the Dart object. - T resurrect(); - - /// Whether or not it is expensive to resurrect this object. - /// - /// Defaults to false. - bool get isResurrectionExpensive => false; -} - -/// A function that restores a Skia object that was temporarily deleted. -typedef Resurrector = T Function(); - -// ignore: avoid_classes_with_only_static_members -/// Singleton that manages the lifecycles of [SkiaObject] instances. -class SkiaObjects { - @visibleForTesting - static final List> resurrectableObjects = - >[]; - - @visibleForTesting - static int maximumCacheSize = 1024; - - @visibleForTesting - static final SkiaObjectCache expensiveCache = - SkiaObjectCache(maximumCacheSize); - - @visibleForTesting - static final List cachesToResize = []; - - static bool _addedCleanupCallback = false; - - @visibleForTesting - static void registerCleanupCallback() { - if (_addedCleanupCallback) { - return; - } - CanvasKitRenderer.instance.rasterizer.addPostFrameCallback(postFrameCleanUp); - _addedCleanupCallback = true; - } - - /// Starts managing the lifecycle of resurrectable [object]. - /// - /// These can safely be deleted at any time. - static void manageResurrectable(ManagedSkiaObject object) { - registerCleanupCallback(); - resurrectableObjects.add(object); - } - - /// Starts managing the lifecycle of a resurrectable object that is expensive. - /// - /// Since it's expensive to resurrect, we shouldn't just delete it after every - /// frame. Instead, add it to a cache and only delete it when the cache fills. - static void manageExpensive(SkiaObject object) { - registerCleanupCallback(); - expensiveCache.add(object); - } - - /// Marks that [cache] has overflown its maximum size and show be resized. - static void markCacheForResize(SkiaObjectCache cache) { - registerCleanupCallback(); - if (cachesToResize.contains(cache)) { - return; - } - cachesToResize.add(cache); - } - - /// Cleans up managed Skia memory. - static void postFrameCleanUp() { - if (resurrectableObjects.isEmpty && cachesToResize.isEmpty) { - return; - } - - for (int i = 0; i < resurrectableObjects.length; i++) { - final SkiaObject object = resurrectableObjects[i]; - object.delete(); - object.didDelete(); - } - resurrectableObjects.clear(); - - for (int i = 0; i < cachesToResize.length; i++) { - final SkiaObjectCache cache = cachesToResize[i]; - cache.resize(); - } - cachesToResize.clear(); - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart index 5c225563a1..8c53334e37 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/vertices.dart @@ -6,11 +6,10 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; -import '../util.dart'; import 'canvaskit_api.dart'; -import 'skia_object_cache.dart'; +import 'native_memory.dart'; -class CkVertices extends ManagedSkiaObject implements ui.Vertices { +class CkVertices implements ui.Vertices { factory CkVertices( ui.VertexMode mode, List positions, { @@ -82,48 +81,31 @@ class CkVertices extends ManagedSkiaObject implements ui.Vertices { this._textureCoordinates, this._colors, this._indices, - ); - - final SkVertexMode _mode; - final Float32List _positions; - final Float32List? _textureCoordinates; - final Uint32List? _colors; - final Uint16List? _indices; - - @override - SkVertices createDefault() { - return canvasKit.MakeVertices( + ) { + final SkVertices skVertices = canvasKit.MakeVertices( _mode, _positions, _textureCoordinates, _colors, _indices, ); + _ref = UniqueRef(this, skVertices, 'Vertices'); } - @override - SkVertices resurrect() { - return createDefault(); - } + final SkVertexMode _mode; + final Float32List _positions; + final Float32List? _textureCoordinates; + final Uint32List? _colors; + final Uint16List? _indices; + late final UniqueRef _ref; - @override - void delete() { - rawSkiaObject?.delete(); - } - - bool _disposed = false; + SkVertices get skiaObject => _ref.nativeObject; @override void dispose() { - delete(); - _disposed = true; + _ref.dispose(); } @override - bool get debugDisposed { - if (assertionsEnabled) { - return _disposed; - } - throw StateError('Vertices.debugDisposed is only avialalbe when asserts are enabled.'); - } + bool get debugDisposed => _ref.isDisposed; } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 36cdf78313..74485512d4 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -43,9 +43,6 @@ void testMain() { test('renders using non-recording canvas if weak refs are supported', () async { - expect(browserSupportsFinalizationRegistry, isTrue, - reason: 'This test specifically tests non-recording canvas, which ' - 'only works if FinalizationRegistry is available.'); final CkPictureRecorder recorder = CkPictureRecorder(); final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); expect(canvas.runtimeType, CkCanvas); @@ -55,38 +52,6 @@ void testMain() { recorder.endRecording(), region: kDefaultRegion, ); - // Safari does not support weak refs (FinalizationRegistry). - // This test should be revisited when Safari ships weak refs. - // TODO(yjbanov): skip Firefox due to a crash: https://github.com/flutter/flutter/issues/86632 - }, skip: isSafari || isFirefox); - - test('renders using a recording canvas if weak refs are not supported', - () async { - browserSupportsFinalizationRegistry = false; - final CkPictureRecorder recorder = CkPictureRecorder(); - final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); - expect(canvas, isA()); - drawTestPicture(canvas); - - final CkPicture originalPicture = recorder.endRecording(); - await matchPictureGolden('canvaskit_picture.png', originalPicture, region: kDefaultRegion); - - final ByteData originalPixels = - (await (await originalPicture.toImage(50, 50)).toByteData())!; - - // Test that a picture restored from a snapshot looks the same. - final CkPictureSnapshot? snapshot = canvas.pictureSnapshot; - expect(snapshot, isNotNull); - final SkPicture restoredSkPicture = snapshot!.toPicture(); - expect(restoredSkPicture, isNotNull); - final CkPicture restoredPicture = CkPicture( - restoredSkPicture, const ui.Rect.fromLTRB(0, 0, 50, 50), snapshot); - final ByteData restoredPixels = - (await (await restoredPicture.toImage(50, 50)).toByteData())!; - - await matchPictureGolden('canvaskit_picture.png', restoredPicture, region: kDefaultRegion); - expect(restoredPixels.buffer.asUint8List(), - originalPixels.buffer.asUint8List()); }); // Regression test for https://github.com/flutter/flutter/issues/51237 @@ -885,9 +850,7 @@ void testMain() { await matchGoldenFile('cross_overlay_resources.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100)); }); - - // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 - }, skip: isSafari || isFirefox); + }); } Future testSampleText(String language, String text, diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index d2140d9f94..748d5e5350 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1438,10 +1438,6 @@ void _canvasTests() { }); test('toImage.toByteData', () async { - // Pretend that FinalizationRegistry is supported, so we can run this - // test in older browsers (the test will use a TestCollector instead of - // ProductionCollector) - browserSupportsFinalizationRegistry = true; final SkPictureRecorder otherRecorder = SkPictureRecorder(); final SkCanvas otherCanvas = otherRecorder .beginRecording(Float32List.fromList([0, 0, 1, 1])); @@ -1450,7 +1446,7 @@ void _canvasTests() { SkPaint()..setColorInt(0xAAFFFFFF), ); final CkPicture picture = - CkPicture(otherRecorder.finishRecordingAsPicture(), null, null); + CkPicture(otherRecorder.finishRecordingAsPicture(), null); final CkImage image = await picture.toImage(1, 1) as CkImage; final ByteData rawData = await image.toByteData(); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart index 68d88c0396..991a77f8ff 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/common.dart @@ -11,38 +11,20 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'package:web_engine_tester/golden_tester.dart'; -/// Used in tests instead of [ProductionCollector] to control Skia object -/// collection explicitly, and to prevent leaks across tests. -/// -/// See [TestCollector] for usage. -late TestCollector testCollector; - const MethodCodec codec = StandardMethodCodec(); /// Common test setup for all CanvasKit unit-tests. void setUpCanvasKitTest() { setUpAll(() async { expect(renderer, isA(), reason: 'This test must run in CanvasKit mode.'); - debugResetBrowserSupportsFinalizationRegistry(); debugDisableFontFallbacks = false; await initializeEngine(assetManager: WebOnlyMockAssetManager()); }); - setUp(() async { - testCollector = TestCollector(); - Collector.debugOverrideCollector(testCollector); - }); - tearDown(() { - testCollector.cleanUpAfterTest(); - debugResetBrowserSupportsFinalizationRegistry(); HtmlViewEmbedder.instance.debugClear(); SurfaceFactory.instance.debugClear(); }); - - tearDownAll(() { - debugResetBrowserSupportsFinalizationRegistry(); - }); } /// Utility function for CanvasKit tests to draw pictures without @@ -55,127 +37,6 @@ CkPicture paintPicture( return recorder.endRecording(); } -class _TestFinalizerRegistration { - _TestFinalizerRegistration(this.wrapper, this.deletable, this.stackTrace); - - final Object wrapper; - final SkDeletable deletable; - final StackTrace stackTrace; -} - -class _TestCollection { - _TestCollection(this.deletable, this.stackTrace); - - final SkDeletable deletable; - final StackTrace stackTrace; -} - -/// Provides explicit synchronous API for collecting Skia objects in tests. -/// -/// [ProductionCollector] relies on `FinalizationRegistry` and timers to -/// delete Skia objects, which makes it more precise and efficient. However, -/// it also makes it unpredictable. For example, an object created in one -/// test may be collected while running another test because the timing is -/// subject to browser-specific GC scheduling. -/// -/// Tests should use [collectNow] and [collectAfterTest] to trigger collections. -class TestCollector implements Collector { - final List<_TestFinalizerRegistration> _activeRegistrations = - <_TestFinalizerRegistration>[]; - final List<_TestFinalizerRegistration> _collectedRegistrations = - <_TestFinalizerRegistration>[]; - - final List<_TestCollection> _pendingCollections = <_TestCollection>[]; - final List<_TestCollection> _completedCollections = <_TestCollection>[]; - - @override - void register(Object wrapper, SkDeletable deletable) { - _activeRegistrations.add( - _TestFinalizerRegistration(wrapper, deletable, StackTrace.current), - ); - } - - @override - void collect(SkDeletable deletable) { - _pendingCollections.add( - _TestCollection(deletable, StackTrace.current), - ); - } - - /// Deletes all Skia objects scheduled for collection. - void collectNow() { - for (final _TestCollection collection in _pendingCollections) { - late final _TestFinalizerRegistration? activeRegistration; - for (final _TestFinalizerRegistration registration in _activeRegistrations) { - if (identical(registration.deletable, collection.deletable)) { - activeRegistration = registration; - break; - } - } - if (activeRegistration == null) { - late final _TestFinalizerRegistration? collectedRegistration; - for (final _TestFinalizerRegistration registration - in _collectedRegistrations) { - if (identical(registration.deletable, collection.deletable)) { - collectedRegistration = registration; - break; - } - } - if (collectedRegistration == null) { - fail( - 'Attempted to collect an object that was never registered for finalization.\n' - 'The collection was requested here:\n' - '${collection.stackTrace}'); - } else { - final _TestCollection firstCollection = _completedCollections - .firstWhere((_TestCollection completedCollection) { - return identical( - completedCollection.deletable, collection.deletable); - }); - fail( - 'Attempted to collect an object that was previously collected.\n' - 'The object was registered for finalization here:\n' - '${collection.stackTrace}\n\n' - 'The first collection was requested here:\n' - '${firstCollection.stackTrace}\n\n' - 'The second collection was requested here:\n' - '${collection.stackTrace}', - ); - } - } else { - _collectedRegistrations.add(activeRegistration); - _activeRegistrations.remove(activeRegistration); - _completedCollections.add(collection); - if (!collection.deletable.isDeleted()) { - collection.deletable.delete(); - } - } - } - _pendingCollections.clear(); - } - - /// Deletes all Skia objects with registered finalizers. - /// - /// This also deletes active objects that have not been scheduled for - /// collection, to prevent objects leaking across tests. - void cleanUpAfterTest() { - for (final _TestCollection collection in _pendingCollections) { - if (!collection.deletable.isDeleted()) { - collection.deletable.delete(); - } - } - for (final _TestFinalizerRegistration registration in _activeRegistrations) { - if (!registration.deletable.isDeleted()) { - registration.deletable.delete(); - } - } - _activeRegistrations.clear(); - _collectedRegistrations.clear(); - _pendingCollections.clear(); - _completedCollections.clear(); - } -} - Future matchSceneGolden(String goldenFile, LayerScene scene, { required ui.Rect region, }) async { diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart index 5f8f83f2f2..f682f2c1e4 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/image_golden_test.dart @@ -95,12 +95,9 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { // Disallow double-dispose. expect(() => image.dispose(), throwsAssertionError); - testCollector.collectNow(); }); - test('CkAnimatedImage remembers last animation position after resurrection', () async { - browserSupportsFinalizationRegistry = false; - + test('CkAnimatedImage iterates frames correctly', () async { final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test'); expect(image.frameCount, 3); expect(image.repetitionCount, -1); @@ -109,16 +106,8 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { await expectFrameData(frame1, [255, 0, 0, 255]); final ui.FrameInfo frame2 = await image.getNextFrame(); await expectFrameData(frame2, [0, 255, 0, 255]); - - // Pretend that the image is temporarily deleted. - image.delete(); - image.didDelete(); - - // Check that we got the 3rd frame after resurrection. final ui.FrameInfo frame3 = await image.getNextFrame(); await expectFrameData(frame3, [0, 0, 255, 255]); - - testCollector.collectNow(); }); test('CkImage toString', () { @@ -128,7 +117,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { final CkImage image = CkImage(skImage); expect(image.toString(), '[1×1]'); image.dispose(); - testCollector.collectNow(); }); test('CkImage can be explicitly disposed of', () { @@ -144,7 +132,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { // Disallow double-dispose. expect(() => image.dispose(), throwsAssertionError); - testCollector.collectNow(); }); test('CkImage can be explicitly disposed of when cloned', () async { @@ -163,22 +150,18 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { expect(image.isCloneOf(clone), isTrue); expect(box.isDisposed, isFalse); - testCollector.collectNow(); expect(skImage.isDeleted(), isFalse); image.dispose(); expect(box.refCount, 1); expect(box.isDisposed, isFalse); - testCollector.collectNow(); expect(skImage.isDeleted(), isFalse); clone.dispose(); expect(box.refCount, 0); expect(box.isDisposed, isTrue); - testCollector.collectNow(); expect(skImage.isDeleted(), isTrue); expect(box.debugGetStackTraces().length, 0); - testCollector.collectNow(); }); test('CkImage toByteData', () async { @@ -188,7 +171,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { final CkImage image = CkImage(skImage); expect((await image.toByteData()).lengthInBytes, greaterThan(0)); expect((await image.toByteData(format: ui.ImageByteFormat.png)).lengthInBytes, greaterThan(0)); - testCollector.collectNow(); }); test('toByteData with decodeImageFromPixels on videoFrame formats', () async { @@ -262,7 +244,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { // The precise PNG encoding is browser-specific, but we can check the file // signature. expect(detectContentType(png.buffer.asUint8List()), 'image/png'); - testCollector.collectNow(); // TODO(hterkelsen): Firefox and Safari do not currently support ImageDecoder. }, skip: isFirefox || isSafari); @@ -282,7 +263,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { final ui.Image image = (await codec.getNextFrame()).image; expect(image.height, 1); expect(image.width, 1); - testCollector.collectNow(); }); test('instantiateImageCodec respects target image size', @@ -312,8 +292,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { image.dispose(); codec.dispose(); } - - testCollector.collectNow(); }); test('instantiateImageCodec with multi-frame image does not support targetWidth/targetHeight', @@ -339,8 +317,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { expect(image.height, 1); image.dispose(); codec.dispose(); - - testCollector.collectNow(); }); test('skiaInstantiateWebImageCodec throws exception on request error', @@ -361,7 +337,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { 'https://flutter.dev/docs/development/platform-integration/web-images', ); } - testCollector.collectNow(); }); test('skiaInstantiateWebImageCodec throws exception on HTTP error', @@ -377,7 +352,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { 'Server response code: 404', ); } - testCollector.collectNow(); }); test('skiaInstantiateWebImageCodec includes URL in the error for malformed image', @@ -409,7 +383,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) { ); } } - testCollector.collectNow(); }); test('Reports error when failing to decode empty image data', () async { @@ -831,7 +804,6 @@ void _testCkAnimatedImage() { // The precise PNG encoding is browser-specific, but we can check the file // signature. expect(detectContentType(png!.buffer.asUint8List()), 'image/png'); - testCollector.collectNow(); }); test('CkAnimatedImage toByteData(RGBA)', () async { @@ -847,7 +819,6 @@ void _testCkAnimatedImage() { expect(rgba, isNotNull); expect(rgba!.buffer.asUint8List(), expectedColors[i]); } - testCollector.collectNow(); }); } @@ -867,7 +838,6 @@ void _testCkBrowserImageDecoder() { // The precise PNG encoding is browser-specific, but we can check the file // signature. expect(detectContentType(png!.buffer.asUint8List()), 'image/png'); - testCollector.collectNow(); }); test('ImageDecoder toByteData(RGBA)', () async { @@ -886,7 +856,6 @@ void _testCkBrowserImageDecoder() { expect(rgba, isNotNull); expect(rgba!.buffer.asUint8List(), expectedColors[i]); } - testCollector.collectNow(); }); test('ImageDecoder expires after inactivity', () async { @@ -930,7 +899,6 @@ void _testCkBrowserImageDecoder() { final ui.FrameInfo frame3 = await image.getNextFrame(); await expectFrameData(frame3, [0, 0, 255, 255]); - testCollector.collectNow(); debugRestoreWebDecoderExpireDuration(); }); } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart index e3f77f9f2c..ef884a2df5 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart @@ -95,7 +95,6 @@ void testMain() { }); test('CkContourMeasure iteration', () { - browserSupportsFinalizationRegistry = false; final ui.Path path = ui.Path(); expect(path, isA()); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); @@ -112,7 +111,6 @@ void testMain() { }); test('CkContourMeasure index', () { - browserSupportsFinalizationRegistry = false; final ui.Path path = ui.Path(); expect(path, isA()); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/picture_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/picture_test.dart index 52de680ee8..ca467d556e 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/picture_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/picture_test.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; +import '../common/matchers.dart'; import 'common.dart'; void main() { @@ -20,19 +21,17 @@ void testMain() { group('CkPicture', () { setUpCanvasKitTest(); - group('in browsers that do not support FinalizationRegistry', () { + group('lifecycle', () { test('can be disposed of manually', () { - browserSupportsFinalizationRegistry = false; - final ui.PictureRecorder recorder = ui.PictureRecorder(); final ui.Canvas canvas = ui.Canvas(recorder); canvas.drawPaint(ui.Paint()); final CkPicture picture = recorder.endRecording() as CkPicture; - expect(picture.rawSkiaObject, isNotNull); + expect(picture.skiaObject, isNotNull); expect(picture.debugDisposed, isFalse); picture.debugCheckNotDisposed('Test.'); // must not throw picture.dispose(); - expect(picture.rawSkiaObject, isNull); + expect(() => picture.skiaObject, throwsA(isAssertionError)); expect(picture.debugDisposed, isTrue); StateError? actualError; @@ -50,36 +49,6 @@ void testMain() { 'The picture has been disposed. ' 'When the picture was disposed the stack trace was:\n' )); - - // Emulate SkiaObjectCache deleting the picture - picture.delete(); - picture.didDelete(); - expect(picture.rawSkiaObject, isNull); - - // A Picture that's been disposed of can no longer be resurrected - expect(() => picture.resurrect(), throwsStateError); - expect(() => picture.toImage(10, 10), throwsStateError); - expect(() => picture.dispose(), throwsStateError); - }); - - test('can be deleted by SkiaObjectCache', () { - browserSupportsFinalizationRegistry = false; - - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final ui.Canvas canvas = ui.Canvas(recorder); - canvas.drawPaint(ui.Paint()); - final CkPicture picture = recorder.endRecording() as CkPicture; - expect(picture.rawSkiaObject, isNotNull); - - // Emulate SkiaObjectCache deleting the picture - picture.delete(); - picture.didDelete(); - expect(picture.rawSkiaObject, isNull); - - // Deletion is softer than disposal. An object may still be resurrected - // if it was deleted prematurely. - expect(picture.debugDisposed, isFalse); - expect(picture.resurrect(), isNotNull); }); }); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart deleted file mode 100644 index 14df75d7e7..0000000000 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:js_interop'; - -import 'package:js/js.dart'; - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart'; - -import 'common.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - group('skia_objects_cache', () { - _tests(); - }); -} - -void _tests() { - SkiaObjects.maximumCacheSize = 4; - - setUpCanvasKitTest(); - - setUp(() async { - // Pretend the browser does not support FinalizationRegistry so we can test the - // resurrection logic. - browserSupportsFinalizationRegistry = false; - }); - - group(ManagedSkiaObject, () { - test('implements create, cache, delete, resurrect, delete lifecycle', () { - final FakeRasterizer fakeRasterizer = FakeRasterizer(); - CanvasKitRenderer.instance.rasterizer = fakeRasterizer; - - // Trigger first create - final TestSkiaObject testObject = TestSkiaObject(); - expect(SkiaObjects.resurrectableObjects.single, testObject); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 0); - - // Check that the getter does not have side-effects - final SkPaint skiaObject1 = testObject.skiaObject; - expect(skiaObject1, isNotNull); - expect(SkiaObjects.resurrectableObjects.single, testObject); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 0); - - // Trigger first delete - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.resurrectableObjects, isEmpty); - expect(fakeRasterizer.addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 1); - - // Trigger resurrect - final SkPaint skiaObject2 = testObject.skiaObject; - expect(skiaObject2, isNotNull); - expect(skiaObject2, isNot(same(skiaObject1))); - expect(SkiaObjects.resurrectableObjects.single, testObject); - expect(fakeRasterizer.addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 1); - expect(testObject.deleteCount, 1); - - // Trigger final delete - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.resurrectableObjects, isEmpty); - expect(fakeRasterizer.addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 1); - expect(testObject.deleteCount, 2); - }); - - test('is added to SkiaObjects cache if expensive', () { - final TestSkiaObject object1 = TestSkiaObject(isExpensive: true); - expect(SkiaObjects.expensiveCache.length, 1); - expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue); - - final TestSkiaObject object2 = TestSkiaObject(isExpensive: true); - expect(SkiaObjects.expensiveCache.length, 2); - expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue); - - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.expensiveCache.length, 2); - expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue); - expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue); - - /// Add 3 more objects to the cache to overflow it. - TestSkiaObject(isExpensive: true); - TestSkiaObject(isExpensive: true); - TestSkiaObject(isExpensive: true); - expect(SkiaObjects.expensiveCache.length, 5); - expect(SkiaObjects.cachesToResize.length, 1); - - SkiaObjects.postFrameCleanUp(); - expect(object1.deleteCount, 1); - expect(object2.deleteCount, 1); - expect(SkiaObjects.expensiveCache.length, 3); - expect(SkiaObjects.expensiveCache.debugContains(object1), isFalse); - expect(SkiaObjects.expensiveCache.debugContains(object2), isFalse); - }); - }); -} - -class TestSkDeletableMock { - static int deleteCount = 0; - - bool isDeleted() => _isDeleted; - bool _isDeleted = false; - - void delete() { - expect(_isDeleted, isFalse, - reason: - 'CanvasKit does not allow deleting the same object more than once.'); - _isDeleted = true; - deleteCount++; - } - - JsConstructor get constructor => TestJsConstructor(name: - 'TestSkDeletable'.toJS); -} - -@JS() -@anonymous -@staticInterop -class TestSkDeletable implements SkDeletable { - factory TestSkDeletable() { - final TestSkDeletableMock mock = TestSkDeletableMock(); - return TestSkDeletable._( - isDeleted: () { return mock.isDeleted(); }.toJS, - delete: () { return mock.delete(); }.toJS, - constructor: mock.constructor); - } - - external factory TestSkDeletable._({ - JSFunction isDeleted, - JSFunction delete, - JsConstructor constructor}); -} - -@JS() -@anonymous -@staticInterop -class TestJsConstructor implements JsConstructor { - external factory TestJsConstructor({JSString name}); -} - -class TestSkiaObject extends ManagedSkiaObject { - TestSkiaObject({this.isExpensive = false}); - - int createDefaultCount = 0; - int resurrectCount = 0; - int deleteCount = 0; - - final bool isExpensive; - - @override - SkPaint createDefault() { - createDefaultCount++; - return SkPaint(); - } - - @override - SkPaint resurrect() { - resurrectCount++; - return SkPaint(); - } - - @override - void delete() { - rawSkiaObject?.delete(); - deleteCount++; - } - - @override - bool get isResurrectionExpensive => isExpensive; -} - -class FakeRasterizer implements Rasterizer { - int addPostFrameCallbackCount = 0; - - @override - void addPostFrameCallback(VoidCallback callback) { - addPostFrameCallbackCount++; - } - - @override - CompositorContext get context => throw UnimplementedError(); - - @override - void draw(LayerTree layerTree) { - throw UnimplementedError(); - } - - @override - void setSkiaResourceCacheMaxBytes(int bytes) { - throw UnimplementedError(); - } - - @override - void debugRunPostFrameCallbacks() { - throw UnimplementedError(); - } -} - -class TestSelfManagedObject extends SkiaObject { - TestSkDeletable? _skiaObject = TestSkDeletable(); - - @override - void delete() { - _skiaObject!.delete(); - } - - @override - void didDelete() { - _skiaObject = null; - } - - @override - TestSkDeletable get skiaObject => throw UnimplementedError(); -} diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/vertices_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/vertices_test.dart index dc0c0110f3..a070522c5f 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/vertices_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/vertices_test.dart @@ -10,6 +10,7 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'package:web_engine_tester/golden_tester.dart'; +import '../common/matchers.dart'; import 'common.dart'; void main() { @@ -20,11 +21,11 @@ void testMain() { group('Vertices', () { setUpCanvasKitTest(); - test('can be constructed, drawn, and deleted', () { + test('can be constructed, drawn, and disposed of', () { final CkVertices vertices = _testVertices(); expect(vertices, isA()); - expect(vertices.createDefault(), isNotNull); - expect(vertices.resurrect(), isNotNull); + expect(vertices.skiaObject, isNotNull); + expect(vertices.debugDisposed, isFalse); final CkPictureRecorder recorder = CkPictureRecorder(); final CkCanvas canvas = @@ -34,7 +35,9 @@ void testMain() { ui.BlendMode.srcOver, CkPaint(), ); - vertices.delete(); + vertices.dispose(); + expect(vertices.debugDisposed, isTrue); + expect(() => vertices.skiaObject, throwsA(isAssertionError)); }); });