mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Cache CanvasKit objects and delete if not used. (flutter/engine#19341)
This commit is contained in:
parent
6fd117d1ae
commit
caa3fb9ff3
@ -431,6 +431,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart
|
||||
@ -440,6 +441,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dar
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/platform_message.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/raster_cache.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/rasterizer.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/surface.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/text.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/util.dart
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
library engine;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection' show ListBase, IterableBase;
|
||||
import 'dart:collection'
|
||||
show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry;
|
||||
import 'dart:convert' hide Codec;
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:html' as html;
|
||||
@ -37,6 +38,7 @@ part 'engine/compositor/initialization.dart';
|
||||
part 'engine/compositor/layer.dart';
|
||||
part 'engine/compositor/layer_scene_builder.dart';
|
||||
part 'engine/compositor/layer_tree.dart';
|
||||
part 'engine/compositor/mask_filter.dart';
|
||||
part 'engine/compositor/n_way_canvas.dart';
|
||||
part 'engine/compositor/path.dart';
|
||||
part 'engine/compositor/painting.dart';
|
||||
@ -46,6 +48,7 @@ part 'engine/compositor/picture_recorder.dart';
|
||||
part 'engine/compositor/platform_message.dart';
|
||||
part 'engine/compositor/raster_cache.dart';
|
||||
part 'engine/compositor/rasterizer.dart';
|
||||
part 'engine/compositor/skia_object_cache.dart';
|
||||
part 'engine/compositor/surface.dart';
|
||||
part 'engine/compositor/text.dart';
|
||||
part 'engine/compositor/util.dart';
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
part of engine;
|
||||
|
||||
/// A Dart wrapper around Skia's SKCanvas.
|
||||
@ -168,7 +167,7 @@ class SkCanvas {
|
||||
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
|
||||
final SkParagraph skParagraph = paragraph as SkParagraph;
|
||||
skCanvas.callMethod('drawParagraph', <dynamic>[
|
||||
skParagraph.skParagraph,
|
||||
skParagraph.skiaObject,
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
]);
|
||||
@ -183,7 +182,8 @@ class SkCanvas {
|
||||
|
||||
void drawPicture(ui.Picture picture) {
|
||||
final SkPicture skPicture = picture as SkPicture;
|
||||
skCanvas.callMethod('drawPicture', <js.JsObject?>[skPicture.skPicture]);
|
||||
skCanvas.callMethod(
|
||||
'drawPicture', <js.JsObject?>[skPicture.skPicture.skiaObject]);
|
||||
}
|
||||
|
||||
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/58824
|
||||
@ -211,8 +211,8 @@ class SkCanvas {
|
||||
|
||||
void drawShadow(ui.Path path, ui.Color color, double elevation,
|
||||
bool transparentOccluder) {
|
||||
drawSkShadow(skCanvas, path as SkPath, color, elevation, transparentOccluder,
|
||||
ui.window.devicePixelRatio);
|
||||
drawSkShadow(skCanvas, path as SkPath, color, elevation,
|
||||
transparentOccluder, ui.window.devicePixelRatio);
|
||||
}
|
||||
|
||||
void drawVertices(
|
||||
@ -243,7 +243,6 @@ class SkCanvas {
|
||||
}
|
||||
|
||||
void saveLayer(ui.Rect bounds, SkPaint paint) {
|
||||
assert(bounds != null, 'Use saveLayerWithoutBounds'); // ignore: unnecessary_null_comparison
|
||||
skCanvas.callMethod('saveLayer', <js.JsObject?>[
|
||||
makeSkRect(bounds),
|
||||
paint.skiaObject,
|
||||
@ -260,7 +259,7 @@ class SkCanvas {
|
||||
'saveLayer',
|
||||
<dynamic>[
|
||||
null,
|
||||
skImageFilter.skImageFilter,
|
||||
skImageFilter.skiaObject,
|
||||
0,
|
||||
makeSkRect(bounds),
|
||||
],
|
||||
|
||||
@ -6,36 +6,52 @@
|
||||
part of engine;
|
||||
|
||||
/// A [ui.ColorFilter] backed by Skia's [SkColorFilter].
|
||||
class SkColorFilter {
|
||||
js.JsObject? skColorFilter;
|
||||
class SkColorFilter extends ResurrectableSkiaObject {
|
||||
final EngineColorFilter _engineFilter;
|
||||
|
||||
SkColorFilter.mode(EngineColorFilter filter) {
|
||||
setSharedSkColor1(filter._color!);
|
||||
skColorFilter =
|
||||
canvasKit['SkColorFilter'].callMethod('MakeBlend', <dynamic>[
|
||||
sharedSkColor1,
|
||||
makeSkBlendMode(filter._blendMode),
|
||||
]);
|
||||
}
|
||||
SkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter;
|
||||
|
||||
SkColorFilter.matrix(EngineColorFilter filter) {
|
||||
// TODO(het): Find a way to remove these array conversions.
|
||||
final js.JsArray<double> colorMatrix = js.JsArray<double>();
|
||||
colorMatrix.length = 20;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
colorMatrix[i] = filter._matrix![i];
|
||||
SkColorFilter.matrix(EngineColorFilter filter) : _engineFilter = filter;
|
||||
|
||||
SkColorFilter.linearToSrgbGamma(EngineColorFilter filter)
|
||||
: _engineFilter = filter;
|
||||
|
||||
SkColorFilter.srgbToLinearGamma(EngineColorFilter filter)
|
||||
: _engineFilter = filter;
|
||||
|
||||
js.JsObject _createSkiaObjectFromFilter() {
|
||||
switch (_engineFilter._type) {
|
||||
case EngineColorFilter._TypeMode:
|
||||
setSharedSkColor1(_engineFilter._color!);
|
||||
return canvasKit['SkColorFilter'].callMethod('MakeBlend', <dynamic>[
|
||||
sharedSkColor1,
|
||||
makeSkBlendMode(_engineFilter._blendMode),
|
||||
]);
|
||||
case EngineColorFilter._TypeMatrix:
|
||||
final js.JsArray<double> colorMatrix = js.JsArray<double>();
|
||||
colorMatrix.length = 20;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
colorMatrix[i] = _engineFilter._matrix![i];
|
||||
}
|
||||
return canvasKit['SkColorFilter']
|
||||
.callMethod('MakeMatrix', <js.JsArray>[colorMatrix]);
|
||||
case EngineColorFilter._TypeLinearToSrgbGamma:
|
||||
return canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma');
|
||||
case EngineColorFilter._TypeSrgbToLinearGamma:
|
||||
return canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma');
|
||||
default:
|
||||
throw StateError(
|
||||
'Unknown mode ${_engineFilter._type} for ColorFilter.');
|
||||
}
|
||||
skColorFilter = canvasKit['SkColorFilter']
|
||||
.callMethod('MakeMatrix', <js.JsArray>[colorMatrix]);
|
||||
}
|
||||
|
||||
SkColorFilter.linearToSrgbGamma(EngineColorFilter filter) {
|
||||
skColorFilter =
|
||||
canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma');
|
||||
@override
|
||||
js.JsObject createDefault() {
|
||||
return _createSkiaObjectFromFilter();
|
||||
}
|
||||
|
||||
SkColorFilter.srgbToLinearGamma(EngineColorFilter filter) {
|
||||
skColorFilter =
|
||||
canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma');
|
||||
@override
|
||||
js.JsObject resurrect() {
|
||||
return _createSkiaObjectFromFilter();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,32 +2,35 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
part of engine;
|
||||
|
||||
/// The CanvasKit implementation of [ui.ImageFilter].
|
||||
///
|
||||
/// Currently only supports `blur`.
|
||||
class SkImageFilter implements ui.ImageFilter {
|
||||
js.JsObject? skImageFilter;
|
||||
|
||||
class SkImageFilter extends ResurrectableSkiaObject implements ui.ImageFilter {
|
||||
SkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0})
|
||||
: _sigmaX = sigmaX,
|
||||
_sigmaY = sigmaY {
|
||||
skImageFilter = canvasKit['SkImageFilter'].callMethod(
|
||||
'MakeBlur',
|
||||
<dynamic>[
|
||||
sigmaX,
|
||||
sigmaY,
|
||||
canvasKit['TileMode']['Clamp'],
|
||||
null,
|
||||
],
|
||||
);
|
||||
}
|
||||
_sigmaY = sigmaY;
|
||||
|
||||
final double _sigmaX;
|
||||
final double _sigmaY;
|
||||
|
||||
@override
|
||||
js.JsObject createDefault() => _initSkiaObject();
|
||||
|
||||
@override
|
||||
js.JsObject resurrect() => _initSkiaObject();
|
||||
|
||||
js.JsObject _initSkiaObject() => canvasKit['SkImageFilter'].callMethod(
|
||||
'MakeBlur',
|
||||
<dynamic>[
|
||||
_sigmaX,
|
||||
_sigmaY,
|
||||
canvasKit['TileMode']['Clamp'],
|
||||
null,
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! SkImageFilter) {
|
||||
|
||||
@ -150,7 +150,8 @@ class ClipPathLayer extends ContainerLayer {
|
||||
assert(needsPainting);
|
||||
|
||||
paintContext.internalNodesCanvas.save();
|
||||
paintContext.internalNodesCanvas.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge);
|
||||
paintContext.internalNodesCanvas
|
||||
.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge);
|
||||
|
||||
if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) {
|
||||
paintContext.internalNodesCanvas.saveLayer(paintBounds, null);
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
part of engine;
|
||||
|
||||
class LayerScene implements ui.Scene {
|
||||
@ -49,7 +48,8 @@ class LayerSceneBuilder implements ui.SceneBuilder {
|
||||
bool isComplexHint = false,
|
||||
bool willChangeHint = false,
|
||||
}) {
|
||||
currentLayer!.add(PictureLayer(picture as SkPicture, offset, isComplexHint, willChangeHint));
|
||||
currentLayer!.add(PictureLayer(
|
||||
picture as SkPicture, offset, isComplexHint, willChangeHint));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
part of engine;
|
||||
|
||||
/// The CanvasKit implementation of [ui.MaskFilter].
|
||||
class SkMaskFilter extends ResurrectableSkiaObject {
|
||||
SkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma)
|
||||
: _blurStyle = blurStyle,
|
||||
_sigma = sigma;
|
||||
|
||||
final ui.BlurStyle _blurStyle;
|
||||
final double _sigma;
|
||||
|
||||
@override
|
||||
js.JsObject createDefault() => _initSkiaObject();
|
||||
|
||||
@override
|
||||
js.JsObject resurrect() => _initSkiaObject();
|
||||
|
||||
js.JsObject _initSkiaObject() {
|
||||
js.JsObject skBlurStyle;
|
||||
switch (_blurStyle) {
|
||||
case ui.BlurStyle.normal:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Normal'];
|
||||
break;
|
||||
case ui.BlurStyle.solid:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Solid'];
|
||||
break;
|
||||
case ui.BlurStyle.outer:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Outer'];
|
||||
break;
|
||||
case ui.BlurStyle.inner:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Inner'];
|
||||
break;
|
||||
}
|
||||
|
||||
return canvasKit
|
||||
.callMethod('MakeBlurMaskFilter', <dynamic>[skBlurStyle, _sigma, true]);
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,7 @@ part of engine;
|
||||
///
|
||||
/// This class is backed by a Skia object that must be explicitly
|
||||
/// deleted to avoid a memory leak. This is done by extending [SkiaObject].
|
||||
class SkPaint extends SkiaObject implements ui.Paint {
|
||||
class SkPaint extends ResurrectableSkiaObject implements ui.Paint {
|
||||
SkPaint();
|
||||
|
||||
static const ui.Color _defaultPaintColor = ui.Color(0xFF000000);
|
||||
@ -22,7 +22,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set blendMode(ui.BlendMode value) {
|
||||
_blendMode = value;
|
||||
_syncBlendMode(skiaObject!);
|
||||
_syncBlendMode(skiaObject);
|
||||
}
|
||||
|
||||
void _syncBlendMode(js.JsObject object) {
|
||||
@ -41,7 +41,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
_syncStyle(skiaObject);
|
||||
}
|
||||
|
||||
void _syncStyle(js.JsObject? object) {
|
||||
void _syncStyle(js.JsObject object) {
|
||||
js.JsObject? skPaintStyle;
|
||||
switch (_style) {
|
||||
case ui.PaintingStyle.stroke:
|
||||
@ -51,7 +51,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
skPaintStyle = _skPaintStyleFill;
|
||||
break;
|
||||
}
|
||||
object!.callMethod('setStyle', <js.JsObject?>[skPaintStyle]);
|
||||
object.callMethod('setStyle', <js.JsObject?>[skPaintStyle]);
|
||||
}
|
||||
|
||||
ui.PaintingStyle _style = ui.PaintingStyle.fill;
|
||||
@ -61,7 +61,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set strokeWidth(double value) {
|
||||
_strokeWidth = value;
|
||||
_syncStrokeWidth(skiaObject!);
|
||||
_syncStrokeWidth(skiaObject);
|
||||
}
|
||||
|
||||
void _syncStrokeWidth(js.JsObject object) {
|
||||
@ -95,7 +95,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set isAntiAlias(bool value) {
|
||||
_isAntiAlias = value;
|
||||
_syncAntiAlias(skiaObject!);
|
||||
_syncAntiAlias(skiaObject);
|
||||
}
|
||||
|
||||
void _syncAntiAlias(js.JsObject object) {
|
||||
@ -109,7 +109,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set color(ui.Color value) {
|
||||
_color = value;
|
||||
_syncColor(skiaObject!);
|
||||
_syncColor(skiaObject);
|
||||
}
|
||||
|
||||
void _syncColor(js.JsObject object) {
|
||||
@ -135,7 +135,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set shader(ui.Shader? value) {
|
||||
_shader = value as EngineShader?;
|
||||
_syncShader(skiaObject!);
|
||||
_syncShader(skiaObject);
|
||||
}
|
||||
|
||||
void _syncShader(js.JsObject object) {
|
||||
@ -156,32 +156,15 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
_syncMaskFilter(skiaObject);
|
||||
}
|
||||
|
||||
void _syncMaskFilter(js.JsObject? object) {
|
||||
js.JsObject? skMaskFilter;
|
||||
void _syncMaskFilter(js.JsObject object) {
|
||||
SkMaskFilter? skMaskFilter;
|
||||
if (_maskFilter != null) {
|
||||
final ui.BlurStyle blurStyle = _maskFilter!.webOnlyBlurStyle;
|
||||
final double sigma = _maskFilter!.webOnlySigma;
|
||||
|
||||
js.JsObject? skBlurStyle;
|
||||
switch (blurStyle) {
|
||||
case ui.BlurStyle.normal:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Normal'];
|
||||
break;
|
||||
case ui.BlurStyle.solid:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Solid'];
|
||||
break;
|
||||
case ui.BlurStyle.outer:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Outer'];
|
||||
break;
|
||||
case ui.BlurStyle.inner:
|
||||
skBlurStyle = canvasKit['BlurStyle']['Inner'];
|
||||
break;
|
||||
}
|
||||
|
||||
skMaskFilter = canvasKit.callMethod(
|
||||
'MakeBlurMaskFilter', <dynamic>[skBlurStyle, sigma, true]);
|
||||
skMaskFilter = SkMaskFilter.blur(blurStyle, sigma);
|
||||
}
|
||||
object!.callMethod('setMaskFilter', <js.JsObject?>[skMaskFilter]);
|
||||
object.callMethod('setMaskFilter', <js.JsObject?>[skMaskFilter?.skiaObject]);
|
||||
}
|
||||
|
||||
ui.MaskFilter? _maskFilter;
|
||||
@ -220,14 +203,14 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set colorFilter(ui.ColorFilter? value) {
|
||||
_colorFilter = value as EngineColorFilter?;
|
||||
_syncColorFilter(skiaObject!);
|
||||
_syncColorFilter(skiaObject);
|
||||
}
|
||||
|
||||
void _syncColorFilter(js.JsObject object) {
|
||||
js.JsObject? skColorFilterJs;
|
||||
if (_colorFilter != null) {
|
||||
SkColorFilter skFilter = _colorFilter!._toSkColorFilter()!;
|
||||
skColorFilterJs = skFilter.skColorFilter;
|
||||
SkColorFilter? skFilter = _colorFilter!._toSkColorFilter();
|
||||
skColorFilterJs = skFilter!.skiaObject;
|
||||
}
|
||||
object.callMethod('setColorFilter', <js.JsObject?>[skColorFilterJs]);
|
||||
}
|
||||
@ -249,13 +232,13 @@ class SkPaint extends SkiaObject implements ui.Paint {
|
||||
@override
|
||||
set imageFilter(ui.ImageFilter? value) {
|
||||
_imageFilter = value as SkImageFilter?;
|
||||
_syncImageFilter(skiaObject!);
|
||||
_syncImageFilter(skiaObject);
|
||||
}
|
||||
|
||||
void _syncImageFilter(js.JsObject object) {
|
||||
js.JsObject? imageFilterJs;
|
||||
if (_imageFilter != null) {
|
||||
imageFilterJs = _imageFilter!.skImageFilter;
|
||||
imageFilterJs = _imageFilter!.skiaObject;
|
||||
}
|
||||
object.callMethod('setImageFilter', <js.JsObject?>[imageFilterJs]);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
part of engine;
|
||||
|
||||
class SkPicture implements ui.Picture {
|
||||
final js.JsObject? skPicture;
|
||||
final SkiaObject skPicture;
|
||||
final ui.Rect? cullRect;
|
||||
|
||||
SkPicture(this.skPicture, this.cullRect);
|
||||
@ -15,7 +15,7 @@ class SkPicture implements ui.Picture {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
skPicture.delete();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
part of engine;
|
||||
|
||||
class SkPictureRecorder implements ui.PictureRecorder {
|
||||
@ -29,7 +28,8 @@ class SkPictureRecorder implements ui.PictureRecorder {
|
||||
_recorder!.callMethod('finishRecordingAsPicture');
|
||||
_recorder!.callMethod('delete');
|
||||
_recorder = null;
|
||||
return SkPicture(skPicture, _cullRect);
|
||||
|
||||
return SkPicture(OneShotSkiaObject(skPicture), _cullRect);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -0,0 +1,279 @@
|
||||
// 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.
|
||||
|
||||
part of engine;
|
||||
|
||||
/// 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 {
|
||||
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<SkiaObject> _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<SkiaObject, DoubleLinkedQueueEntry<SkiaObject>> _itemMap;
|
||||
|
||||
SkiaObjectCache(this.maximumSize)
|
||||
: _itemQueue = DoubleLinkedQueue<SkiaObject>(),
|
||||
_itemMap = <SkiaObject, DoubleLinkedQueueEntry<SkiaObject>>{};
|
||||
|
||||
/// 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) {
|
||||
DoubleLinkedQueueEntry<SkiaObject> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object backed by a [js.JsObject] 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.
|
||||
js.JsObject? get skiaObject;
|
||||
|
||||
/// Deletes the associated C++ object from the WebAssembly heap.
|
||||
void delete();
|
||||
}
|
||||
|
||||
/// A [SkiaObject] that can resurrect its C++ counterpart.
|
||||
///
|
||||
/// Because there is no feedback from JavaScript's GC (no destructors or
|
||||
/// finalizers), we pessimistically delete the underlying C++ object before
|
||||
/// the Dart object is garbage-collected. The current algorithm deletes objects
|
||||
/// at the end of every frame. This allows reusing the C++ objects within the
|
||||
/// frame. In the future we may add smarter strategies that will allow us to
|
||||
/// reuse C++ objects across frames.
|
||||
///
|
||||
/// The lifecycle of a C++ object is as follows:
|
||||
///
|
||||
/// - Create default: when instantiating a C++ object for a Dart object for the
|
||||
/// first time, the C++ object is populated with default data (the defaults are
|
||||
/// defined by Flutter; Skia defaults are corrected if necessary). The
|
||||
/// default object is created by [createDefault].
|
||||
/// - 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 ResurrectableSkiaObject extends SkiaObject {
|
||||
ResurrectableSkiaObject() {
|
||||
_skiaObject = createDefault();
|
||||
if (isResurrectionExpensive) {
|
||||
SkiaObjects.manageExpensive(this);
|
||||
} else {
|
||||
SkiaObjects.manageResurrectable(this);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
js.JsObject get skiaObject {
|
||||
if (_skiaObject == null) {
|
||||
_skiaObject = resurrect();
|
||||
if (isResurrectionExpensive) {
|
||||
SkiaObjects.manageExpensive(this);
|
||||
} else {
|
||||
SkiaObjects.manageResurrectable(this);
|
||||
}
|
||||
}
|
||||
return _skiaObject!;
|
||||
}
|
||||
|
||||
/// Do not use this field outside this class. Use [skiaObject] instead.
|
||||
js.JsObject? _skiaObject;
|
||||
|
||||
/// 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.
|
||||
js.JsObject createDefault();
|
||||
|
||||
/// Creates a new Skia-backed JavaScript object containing data representing
|
||||
/// the current state of the Dart object.
|
||||
js.JsObject resurrect();
|
||||
|
||||
/// Whether or not it is expensive to resurrect this object.
|
||||
///
|
||||
/// Defaults to false.
|
||||
bool get isResurrectionExpensive => false;
|
||||
|
||||
@override
|
||||
void delete() {
|
||||
_skiaObject!.callMethod('delete');
|
||||
_skiaObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(hterkelsen): [OneShotSkiaObject] is dangerous because it might delete
|
||||
// the underlying Skia object while the associated Dart object is still in
|
||||
// use. This issue discusses ways to address this:
|
||||
// https://github.com/flutter/flutter/issues/60401
|
||||
/// A [SkiaObject] which is deleted once and cannot be used again.
|
||||
class OneShotSkiaObject extends SkiaObject {
|
||||
js.JsObject? _skiaObject;
|
||||
|
||||
OneShotSkiaObject(this._skiaObject) {
|
||||
SkiaObjects.manageOneShot(this);
|
||||
}
|
||||
|
||||
@override
|
||||
js.JsObject? get skiaObject {
|
||||
if (_skiaObject == null) {
|
||||
throw StateError('Attempting to use a Skia object that has been freed.');
|
||||
}
|
||||
SkiaObjects.oneShotCache.markUsed(this);
|
||||
return _skiaObject;
|
||||
}
|
||||
|
||||
@override
|
||||
void delete() {
|
||||
_skiaObject!.callMethod('delete');
|
||||
_skiaObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Singleton that manages the lifecycles of [SkiaObject] instances.
|
||||
class SkiaObjects {
|
||||
// TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects
|
||||
// beyond a single frame.
|
||||
@visibleForTesting
|
||||
static final List<ResurrectableSkiaObject> resurrectableObjects =
|
||||
<ResurrectableSkiaObject>[];
|
||||
|
||||
@visibleForTesting
|
||||
static int maximumCacheSize = 8192;
|
||||
|
||||
@visibleForTesting
|
||||
static final SkiaObjectCache oneShotCache = SkiaObjectCache(maximumCacheSize);
|
||||
|
||||
@visibleForTesting
|
||||
static final SkiaObjectCache expensiveCache =
|
||||
SkiaObjectCache(maximumCacheSize);
|
||||
|
||||
@visibleForTesting
|
||||
static final List<SkiaObjectCache> cachesToResize = <SkiaObjectCache>[];
|
||||
|
||||
static bool _addedCleanupCallback = false;
|
||||
|
||||
@visibleForTesting
|
||||
static void registerCleanupCallback() {
|
||||
if (_addedCleanupCallback) {
|
||||
return;
|
||||
}
|
||||
window.rasterizer!.addPostFrameCallback(postFrameCleanUp);
|
||||
_addedCleanupCallback = true;
|
||||
}
|
||||
|
||||
/// Starts managing the lifecycle of resurrectable [object].
|
||||
///
|
||||
/// These can safely be deleted at any time.
|
||||
static void manageResurrectable(ResurrectableSkiaObject object) {
|
||||
registerCleanupCallback();
|
||||
resurrectableObjects.add(object);
|
||||
}
|
||||
|
||||
/// Starts managing the lifecycle of a one-shot [object].
|
||||
///
|
||||
/// We should avoid deleting these whenever we can, since we won't
|
||||
/// be able to resurrect them.
|
||||
static void manageOneShot(OneShotSkiaObject object) {
|
||||
registerCleanupCallback();
|
||||
oneShotCache.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(ResurrectableSkiaObject 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();
|
||||
}
|
||||
resurrectableObjects.clear();
|
||||
|
||||
for (int i = 0; i < cachesToResize.length; i++) {
|
||||
final SkiaObjectCache cache = cachesToResize[i];
|
||||
cache.resize();
|
||||
}
|
||||
cachesToResize.clear();
|
||||
}
|
||||
}
|
||||
@ -79,9 +79,8 @@ class Surface {
|
||||
|
||||
final SkSurface? currentSurface = _surface;
|
||||
if (currentSurface != null) {
|
||||
final bool isSameSize =
|
||||
size.width == currentSurface.width() &&
|
||||
size.height == currentSurface.height();
|
||||
final bool isSameSize = size.width == currentSurface.width() &&
|
||||
size.height == currentSurface.height();
|
||||
if (isSameSize) {
|
||||
// The existing surface is still reusable.
|
||||
return;
|
||||
@ -131,7 +130,7 @@ class Surface {
|
||||
}
|
||||
|
||||
htmlElement = htmlCanvas;
|
||||
return SkSurface(skSurface, glContext);
|
||||
return SkSurface(skSurface, grContext, glContext);
|
||||
}
|
||||
|
||||
bool _presentSurface() {
|
||||
@ -144,9 +143,10 @@ class Surface {
|
||||
/// A Dart wrapper around Skia's SkSurface.
|
||||
class SkSurface {
|
||||
final js.JsObject _surface;
|
||||
final js.JsObject _grContext;
|
||||
final int _glContext;
|
||||
|
||||
SkSurface(this._surface, this._glContext);
|
||||
SkSurface(this._surface, this._grContext, this._glContext);
|
||||
|
||||
SkCanvas getCanvas() {
|
||||
final js.JsObject skCanvas = _surface.callMethod('getCanvas');
|
||||
@ -160,5 +160,7 @@ class SkSurface {
|
||||
|
||||
void dispose() {
|
||||
_surface.callMethod('dispose');
|
||||
_grContext.callMethod('releaseResourcesAndAbandonContext');
|
||||
_grContext.callMethod('delete');
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,40 +282,90 @@ Map<String, js.JsObject?> toSkFontStyle(
|
||||
return style;
|
||||
}
|
||||
|
||||
class SkParagraph implements ui.Paragraph {
|
||||
SkParagraph(this.skParagraph, this._textDirection, this._fontFamily);
|
||||
class SkParagraph extends ResurrectableSkiaObject implements ui.Paragraph {
|
||||
SkParagraph(
|
||||
this._initialParagraph, this._paragraphStyle, this._paragraphCommands);
|
||||
|
||||
final js.JsObject? skParagraph;
|
||||
final ui.TextDirection? _textDirection;
|
||||
final String? _fontFamily;
|
||||
/// The result of calling `build()` on the JS SkParagraphBuilder.
|
||||
///
|
||||
/// This may be invalidated later.
|
||||
final js.JsObject _initialParagraph;
|
||||
|
||||
/// The paragraph style used to build this paragraph.
|
||||
///
|
||||
/// This is used to resurrect the paragraph if the initial paragraph
|
||||
/// is deleted.
|
||||
final SkParagraphStyle _paragraphStyle;
|
||||
|
||||
/// The paragraph builder commands used to build this paragraph.
|
||||
///
|
||||
/// This is used to resurrect the paragraph if the initial paragraph
|
||||
/// is deleted.
|
||||
final List<_ParagraphCommand> _paragraphCommands;
|
||||
|
||||
/// The constraints from the last time we layed the paragraph out.
|
||||
///
|
||||
/// This is used to resurrect the paragraph if the initial paragraph
|
||||
/// is deleted.
|
||||
ui.ParagraphConstraints? _lastLayoutConstraints;
|
||||
|
||||
@override
|
||||
js.JsObject createDefault() => _initialParagraph;
|
||||
|
||||
@override
|
||||
js.JsObject resurrect() {
|
||||
final builder = SkParagraphBuilder(_paragraphStyle);
|
||||
for (_ParagraphCommand command in _paragraphCommands) {
|
||||
switch (command.type) {
|
||||
case _ParagraphCommandType.addText:
|
||||
builder.addText(command.text!);
|
||||
break;
|
||||
case _ParagraphCommandType.pop:
|
||||
builder.pop();
|
||||
break;
|
||||
case _ParagraphCommandType.pushStyle:
|
||||
builder.pushStyle(command.style!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final js.JsObject result = builder._buildSkParagraph();
|
||||
if (_lastLayoutConstraints != null) {
|
||||
// We need to set the Skia object early so layout works.
|
||||
_skiaObject = result;
|
||||
this.layout(_lastLayoutConstraints!);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isResurrectionExpensive => true;
|
||||
|
||||
@override
|
||||
double get alphabeticBaseline =>
|
||||
skParagraph!.callMethod('getAlphabeticBaseline');
|
||||
skiaObject.callMethod('getAlphabeticBaseline');
|
||||
|
||||
@override
|
||||
bool get didExceedMaxLines => skParagraph!.callMethod('didExceedMaxLines');
|
||||
bool get didExceedMaxLines => skiaObject.callMethod('didExceedMaxLines');
|
||||
|
||||
@override
|
||||
double get height => skParagraph!.callMethod('getHeight');
|
||||
double get height => skiaObject.callMethod('getHeight');
|
||||
|
||||
@override
|
||||
double get ideographicBaseline =>
|
||||
skParagraph!.callMethod('getIdeographicBaseline');
|
||||
skiaObject.callMethod('getIdeographicBaseline');
|
||||
|
||||
@override
|
||||
double get longestLine => skParagraph!.callMethod('getLongestLine');
|
||||
double get longestLine => skiaObject.callMethod('getLongestLine');
|
||||
|
||||
@override
|
||||
double get maxIntrinsicWidth =>
|
||||
skParagraph!.callMethod('getMaxIntrinsicWidth');
|
||||
double get maxIntrinsicWidth => skiaObject.callMethod('getMaxIntrinsicWidth');
|
||||
|
||||
@override
|
||||
double get minIntrinsicWidth =>
|
||||
skParagraph!.callMethod('getMinIntrinsicWidth');
|
||||
double get minIntrinsicWidth => skiaObject.callMethod('getMinIntrinsicWidth');
|
||||
|
||||
@override
|
||||
double get width => skParagraph!.callMethod('getMaxWidth');
|
||||
double get width => skiaObject.callMethod('getMaxWidth');
|
||||
|
||||
// TODO(hterkelsen): Implement placeholders once it's in CanvasKit
|
||||
@override
|
||||
@ -361,7 +411,7 @@ class SkParagraph implements ui.Paragraph {
|
||||
}
|
||||
|
||||
List<js.JsObject> skRects =
|
||||
skParagraph!.callMethod('getRectsForRange', <dynamic>[
|
||||
skiaObject.callMethod('getRectsForRange', <dynamic>[
|
||||
start,
|
||||
end,
|
||||
heightStyle,
|
||||
@ -377,7 +427,7 @@ class SkParagraph implements ui.Paragraph {
|
||||
rect['fTop'],
|
||||
rect['fRight'],
|
||||
rect['fBottom'],
|
||||
_textDirection!,
|
||||
_paragraphStyle._textDirection!,
|
||||
));
|
||||
}
|
||||
|
||||
@ -387,7 +437,7 @@ class SkParagraph implements ui.Paragraph {
|
||||
@override
|
||||
ui.TextPosition getPositionForOffset(ui.Offset offset) {
|
||||
js.JsObject positionWithAffinity =
|
||||
skParagraph!.callMethod('getGlyphPositionAtCoordinate', <double>[
|
||||
skiaObject.callMethod('getGlyphPositionAtCoordinate', <double>[
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
]);
|
||||
@ -397,13 +447,14 @@ class SkParagraph implements ui.Paragraph {
|
||||
@override
|
||||
ui.TextRange getWordBoundary(ui.TextPosition position) {
|
||||
js.JsObject skRange =
|
||||
skParagraph!.callMethod('getWordBoundary', <int>[position.offset]);
|
||||
skiaObject.callMethod('getWordBoundary', <int>[position.offset]);
|
||||
return ui.TextRange(start: skRange['start'], end: skRange['end']);
|
||||
}
|
||||
|
||||
@override
|
||||
void layout(ui.ParagraphConstraints constraints) {
|
||||
assert(constraints.width != null); // ignore: unnecessary_null_comparison
|
||||
_lastLayoutConstraints = constraints;
|
||||
|
||||
// Infinite width breaks layout, just use a very large number instead.
|
||||
// TODO(het): Remove this once https://bugs.chromium.org/p/skia/issues/detail?id=9874
|
||||
@ -418,10 +469,11 @@ class SkParagraph implements ui.Paragraph {
|
||||
// TODO(het): CanvasKit throws an exception when laid out with
|
||||
// a font that wasn't registered.
|
||||
try {
|
||||
skParagraph!.callMethod('layout', <double>[width]);
|
||||
skiaObject.callMethod('layout', <double>[width]);
|
||||
} catch (e) {
|
||||
html.window.console.warn('CanvasKit threw an exception while laying '
|
||||
'out the paragraph. The font was "$_fontFamily". Exception:\n$e');
|
||||
'out the paragraph. The font was "${_paragraphStyle._fontFamily}". '
|
||||
'Exception:\n$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -441,17 +493,16 @@ class SkParagraph implements ui.Paragraph {
|
||||
|
||||
class SkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
js.JsObject? _paragraphBuilder;
|
||||
ui.TextDirection? _textDirection;
|
||||
String? _fontFamily;
|
||||
final SkParagraphStyle _style;
|
||||
final List<_ParagraphCommand> _commands;
|
||||
|
||||
SkParagraphBuilder(ui.ParagraphStyle style) {
|
||||
SkParagraphStyle skStyle = style as SkParagraphStyle;
|
||||
_textDirection = skStyle._textDirection;
|
||||
_fontFamily = skStyle._fontFamily;
|
||||
SkParagraphBuilder(ui.ParagraphStyle style)
|
||||
: _commands = <_ParagraphCommand>[],
|
||||
_style = style as SkParagraphStyle {
|
||||
_paragraphBuilder = canvasKit['ParagraphBuilder'].callMethod(
|
||||
'Make',
|
||||
<js.JsObject?>[
|
||||
skStyle.skParagraphStyle,
|
||||
_style.skParagraphStyle,
|
||||
skiaFontCollection.skFontMgr,
|
||||
],
|
||||
);
|
||||
@ -472,16 +523,22 @@ class SkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
|
||||
@override
|
||||
void addText(String text) {
|
||||
_commands.add(_ParagraphCommand.addText(text));
|
||||
_paragraphBuilder!.callMethod('addText', <String>[text]);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Paragraph build() {
|
||||
final SkParagraph paragraph = SkParagraph(
|
||||
_paragraphBuilder!.callMethod('build'), _textDirection, _fontFamily);
|
||||
final builtParagraph = _buildSkParagraph();
|
||||
return SkParagraph(builtParagraph, _style, _commands);
|
||||
}
|
||||
|
||||
/// Builds the SkParagraph with the builder and deletes the builder.
|
||||
js.JsObject _buildSkParagraph() {
|
||||
final js.JsObject result = _paragraphBuilder!.callMethod('build');
|
||||
_paragraphBuilder!.callMethod('delete');
|
||||
_paragraphBuilder = null;
|
||||
return paragraph;
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -493,13 +550,37 @@ class SkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
|
||||
@override
|
||||
void pop() {
|
||||
_commands.add(const _ParagraphCommand.pop());
|
||||
_paragraphBuilder!.callMethod('pop');
|
||||
}
|
||||
|
||||
@override
|
||||
void pushStyle(ui.TextStyle style) {
|
||||
final SkTextStyle skStyle = style as SkTextStyle;
|
||||
_commands.add(_ParagraphCommand.pushStyle(skStyle));
|
||||
_paragraphBuilder!
|
||||
.callMethod('pushStyle', <js.JsObject?>[skStyle.skTextStyle]);
|
||||
}
|
||||
}
|
||||
|
||||
class _ParagraphCommand {
|
||||
final _ParagraphCommandType type;
|
||||
final String? text;
|
||||
final SkTextStyle? style;
|
||||
|
||||
const _ParagraphCommand._(this.type, this.text, this.style);
|
||||
|
||||
const _ParagraphCommand.addText(String text)
|
||||
: this._(_ParagraphCommandType.addText, text, null);
|
||||
|
||||
const _ParagraphCommand.pop() : this._(_ParagraphCommandType.pop, null, null);
|
||||
|
||||
const _ParagraphCommand.pushStyle(SkTextStyle style)
|
||||
: this._(_ParagraphCommandType.pushStyle, null, style);
|
||||
}
|
||||
|
||||
enum _ParagraphCommandType {
|
||||
addText,
|
||||
pop,
|
||||
pushStyle,
|
||||
}
|
||||
|
||||
@ -15,97 +15,6 @@ class CanvasKitError extends Error {
|
||||
String toString() => 'CanvasKitError: $message';
|
||||
}
|
||||
|
||||
/// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the
|
||||
/// WebAssembly heap.
|
||||
///
|
||||
/// These objects are automatically deleted when no longer used.
|
||||
///
|
||||
/// Because there is no feedback from JavaScript's GC (no destructors or
|
||||
/// finalizers), we pessimistically delete the underlying C++ object before
|
||||
/// the Dart object is garbage-collected. The current algorithm deletes objects
|
||||
/// at the end of every frame. This allows reusing the C++ objects within the
|
||||
/// frame. In the future we may add smarter strategies that will allow us to
|
||||
/// reuse C++ objects across frames.
|
||||
///
|
||||
/// The lifecycle of a C++ object is as follows:
|
||||
///
|
||||
/// - Create default: when instantiating a C++ object for a Dart object for the
|
||||
/// first time, the C++ object is populated with default data (the defaults are
|
||||
/// defined by Flutter; Skia defaults are corrected if necessary). The
|
||||
/// default object is created by [createDefault].
|
||||
/// - 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 SkiaObject {
|
||||
SkiaObject() {
|
||||
_skiaObject = createDefault();
|
||||
SkiaObjects.manage(this);
|
||||
}
|
||||
|
||||
/// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap.
|
||||
js.JsObject? get skiaObject {
|
||||
if (_skiaObject == null) {
|
||||
_skiaObject = resurrect();
|
||||
SkiaObjects.manage(this);
|
||||
}
|
||||
return _skiaObject;
|
||||
}
|
||||
|
||||
/// Do not use this field outside this class. Use [skiaObject] instead.
|
||||
js.JsObject? _skiaObject;
|
||||
|
||||
/// 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.
|
||||
js.JsObject createDefault();
|
||||
|
||||
/// Creates a new Skia-backed JavaScript object containing data representing
|
||||
/// the current state of the Dart object.
|
||||
js.JsObject resurrect();
|
||||
}
|
||||
|
||||
/// Singleton that manages the lifecycles of [SkiaObject] instances.
|
||||
class SkiaObjects {
|
||||
// TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects
|
||||
// beyond a single frame.
|
||||
@visibleForTesting
|
||||
static final List<SkiaObject> managedObjects = () {
|
||||
window.rasterizer!.addPostFrameCallback(postFrameCleanUp);
|
||||
return <SkiaObject>[];
|
||||
}();
|
||||
|
||||
/// Starts managing the lifecycle of [object].
|
||||
///
|
||||
/// The object's underlying WASM object is deleted by calling the
|
||||
/// "delete" method when it goes out of scope.
|
||||
///
|
||||
/// The current implementation deletes objects at the end of every frame.
|
||||
static void manage(SkiaObject object) {
|
||||
managedObjects.add(object);
|
||||
}
|
||||
|
||||
/// Deletes all C++ objects created this frame.
|
||||
static void postFrameCleanUp() {
|
||||
if (managedObjects.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < managedObjects.length; i++) {
|
||||
final SkiaObject object = managedObjects[i];
|
||||
object._skiaObject!.callMethod('delete');
|
||||
object._skiaObject = null;
|
||||
}
|
||||
|
||||
managedObjects.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a list of [ui.Color] into the 2d array expected by CanvasKit.
|
||||
js.JsArray<Float32List> makeColorList(List<ui.Color> colors) {
|
||||
var result = js.JsArray<Float32List>();
|
||||
|
||||
@ -0,0 +1,170 @@
|
||||
// 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.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
void main() {
|
||||
SkiaObjects.maximumCacheSize = 4;
|
||||
group(ResurrectableSkiaObject, () {
|
||||
test('implements create, cache, delete, resurrect, delete lifecycle', () {
|
||||
int addPostFrameCallbackCount = 0;
|
||||
|
||||
MockRasterizer mockRasterizer = MockRasterizer();
|
||||
when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) {
|
||||
addPostFrameCallbackCount++;
|
||||
});
|
||||
window.rasterizer = mockRasterizer;
|
||||
|
||||
// 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 JsObject 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(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 0);
|
||||
expect(testObject.deleteCount, 1);
|
||||
|
||||
// Trigger resurrect
|
||||
final JsObject skiaObject2 = testObject.skiaObject;
|
||||
expect(skiaObject2, isNotNull);
|
||||
expect(skiaObject2, isNot(same(skiaObject1)));
|
||||
expect(SkiaObjects.resurrectableObjects.single, testObject);
|
||||
expect(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 1);
|
||||
expect(testObject.deleteCount, 1);
|
||||
|
||||
// Trigger final delete
|
||||
SkiaObjects.postFrameCleanUp();
|
||||
expect(SkiaObjects.resurrectableObjects, isEmpty);
|
||||
expect(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 1);
|
||||
expect(testObject.deleteCount, 2);
|
||||
});
|
||||
|
||||
test('is added to SkiaObjects cache if expensive', () {
|
||||
TestSkiaObject object1 = TestSkiaObject(isExpensive: true);
|
||||
expect(SkiaObjects.expensiveCache.length, 1);
|
||||
expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
group(OneShotSkiaObject, () {
|
||||
test('is added to SkiaObjects cache', () {
|
||||
int deleteCount = 0;
|
||||
JsObject _makeJsObject() {
|
||||
return JsObject.jsify({
|
||||
'delete': allowInterop(() {
|
||||
deleteCount++;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
OneShotSkiaObject object1 = OneShotSkiaObject(_makeJsObject());
|
||||
expect(SkiaObjects.oneShotCache.length, 1);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue);
|
||||
|
||||
OneShotSkiaObject object2 = OneShotSkiaObject(_makeJsObject());
|
||||
expect(SkiaObjects.oneShotCache.length, 2);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue);
|
||||
|
||||
SkiaObjects.postFrameCleanUp();
|
||||
expect(SkiaObjects.oneShotCache.length, 2);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue);
|
||||
|
||||
// Add 3 more objects to the cache to overflow it.
|
||||
OneShotSkiaObject(_makeJsObject());
|
||||
OneShotSkiaObject(_makeJsObject());
|
||||
OneShotSkiaObject(_makeJsObject());
|
||||
expect(SkiaObjects.oneShotCache.length, 5);
|
||||
expect(SkiaObjects.cachesToResize.length, 1);
|
||||
|
||||
SkiaObjects.postFrameCleanUp();
|
||||
expect(deleteCount, 2);
|
||||
expect(SkiaObjects.oneShotCache.length, 3);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object1), isFalse);
|
||||
expect(SkiaObjects.oneShotCache.debugContains(object2), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestSkiaObject extends ResurrectableSkiaObject {
|
||||
int createDefaultCount = 0;
|
||||
int resurrectCount = 0;
|
||||
int deleteCount = 0;
|
||||
|
||||
final bool isExpensive;
|
||||
|
||||
TestSkiaObject({this.isExpensive = false});
|
||||
|
||||
JsObject _makeJsObject() {
|
||||
return JsObject.jsify({
|
||||
'delete': allowInterop(() {
|
||||
deleteCount++;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
JsObject createDefault() {
|
||||
createDefaultCount++;
|
||||
return _makeJsObject();
|
||||
}
|
||||
|
||||
@override
|
||||
JsObject resurrect() {
|
||||
resurrectCount++;
|
||||
return _makeJsObject();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isResurrectionExpensive => isExpensive;
|
||||
}
|
||||
|
||||
class MockRasterizer extends Mock implements Rasterizer {}
|
||||
@ -1,94 +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.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
void main() {
|
||||
group(SkiaObject, () {
|
||||
test('implements create, cache, delete, resurrect, delete lifecycle', () {
|
||||
int addPostFrameCallbackCount = 0;
|
||||
|
||||
MockRasterizer mockRasterizer = MockRasterizer();
|
||||
when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) {
|
||||
addPostFrameCallbackCount++;
|
||||
});
|
||||
window.rasterizer = mockRasterizer;
|
||||
|
||||
// Trigger first create
|
||||
final TestSkiaObject testObject = TestSkiaObject();
|
||||
expect(SkiaObjects.managedObjects.single, testObject);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 0);
|
||||
expect(testObject.deleteCount, 0);
|
||||
|
||||
// Check that the getter does not have side-effects
|
||||
final JsObject skiaObject1 = testObject.skiaObject;
|
||||
expect(skiaObject1, isNotNull);
|
||||
expect(SkiaObjects.managedObjects.single, testObject);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 0);
|
||||
expect(testObject.deleteCount, 0);
|
||||
|
||||
// Trigger first delete
|
||||
SkiaObjects.postFrameCleanUp();
|
||||
expect(SkiaObjects.managedObjects, isEmpty);
|
||||
expect(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 0);
|
||||
expect(testObject.deleteCount, 1);
|
||||
|
||||
// Trigger resurrect
|
||||
final JsObject skiaObject2 = testObject.skiaObject;
|
||||
expect(skiaObject2, isNotNull);
|
||||
expect(skiaObject2, isNot(same(skiaObject1)));
|
||||
expect(SkiaObjects.managedObjects.single, testObject);
|
||||
expect(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 1);
|
||||
expect(testObject.deleteCount, 1);
|
||||
|
||||
// Trigger final delete
|
||||
SkiaObjects.postFrameCleanUp();
|
||||
expect(SkiaObjects.managedObjects, isEmpty);
|
||||
expect(addPostFrameCallbackCount, 1);
|
||||
expect(testObject.createDefaultCount, 1);
|
||||
expect(testObject.resurrectCount, 1);
|
||||
expect(testObject.deleteCount, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestSkiaObject extends SkiaObject {
|
||||
int createDefaultCount = 0;
|
||||
int resurrectCount = 0;
|
||||
int deleteCount = 0;
|
||||
|
||||
JsObject _makeJsObject() {
|
||||
return JsObject.jsify({
|
||||
'delete': allowInterop(() {
|
||||
deleteCount++;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
JsObject createDefault() {
|
||||
createDefaultCount++;
|
||||
return _makeJsObject();
|
||||
}
|
||||
|
||||
@override
|
||||
JsObject resurrect() {
|
||||
resurrectCount++;
|
||||
return _makeJsObject();
|
||||
}
|
||||
}
|
||||
|
||||
class MockRasterizer extends Mock implements Rasterizer {}
|
||||
Loading…
x
Reference in New Issue
Block a user