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 c6c0d2ebe5..51470249d2 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 @@ -1472,6 +1472,12 @@ class SkImageFilter {} extension SkImageFilterExtension on SkImageFilter { external JSVoid delete(); + + + @JS('getOutputBounds') + external JSInt32Array _getOutputBounds(JSFloat32Array bounds); + Int32List getOutputBounds(Float32List bounds) => + _getOutputBounds(bounds.toJS).toDart; } @JS() @@ -2195,8 +2201,10 @@ class SkPictureRecorder { extension SkPictureRecorderExtension on SkPictureRecorder { @JS('beginRecording') - external SkCanvas _beginRecording(JSFloat32Array bounds); - SkCanvas beginRecording(Float32List bounds) => _beginRecording(bounds.toJS); + external SkCanvas _beginRecording( + JSFloat32Array bounds, JSBoolean computeBounds); + SkCanvas beginRecording(Float32List bounds) => + _beginRecording(bounds.toJS, true.toJS); external SkPicture finishRecordingAsPicture(); external JSVoid delete(); @@ -2594,6 +2602,14 @@ class SkPicture {} extension SkPictureExtension on SkPicture { external JSVoid delete(); + + @JS('cullRect') + external JSFloat32Array _cullRect(); + Float32List cullRect() => _cullRect().toDart; + + @JS('approximateBytesUsed') + external JSNumber _approximateBytesUsed(); + int approximateBytesUsed() => _approximateBytesUsed().toDartInt; } @JS() diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 70a1a3cb1d..779ca9babb 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -16,6 +16,7 @@ import '../window.dart'; import 'canvas.dart'; import 'embedded_views_diff.dart'; import 'path.dart'; +import 'picture.dart'; import 'picture_recorder.dart'; import 'renderer.dart'; import 'surface.dart'; @@ -139,7 +140,6 @@ class HtmlViewEmbedder { if (needNewOverlay && hasAvailableOverlay) { final CkPictureRecorder pictureRecorder = CkPictureRecorder(); pictureRecorder.beginRecording(ui.Offset.zero & _frameSize); - pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); _context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder); } @@ -422,9 +422,10 @@ class HtmlViewEmbedder { if (_overlays[viewId] != null) { final SurfaceFrame frame = _overlays[viewId]!.acquireFrame(_frameSize); final CkCanvas canvas = frame.skiaCanvas; - canvas.drawPicture( - _context.pictureRecorders[pictureRecorderIndex].endRecording(), - ); + final CkPicture ckPicture = + _context.pictureRecorders[pictureRecorderIndex].endRecording(); + canvas.clear(const ui.Color(0x00000000)); + canvas.drawPicture(ckPicture); pictureRecorderIndex++; frame.submit(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart index 314aa4870b..9a8c37b75e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -6,6 +6,7 @@ import 'package:ui/ui.dart' as ui; import '../vector_math.dart'; import 'canvas.dart'; +import 'canvaskit_api.dart'; import 'embedded_views.dart'; import 'image_filter.dart'; import 'n_way_canvas.dart'; @@ -399,12 +400,17 @@ class ImageFilterEngineLayer extends ContainerLayer @override void preroll(PrerollContext prerollContext, Matrix4 matrix) { - final Matrix4 transform = (_filter as CkManagedSkImageFilterConvertible).transform; + final Matrix4 transform = + (_filter as CkManagedSkImageFilterConvertible).transform; final Matrix4 childMatrix = matrix.multiplied(transform); prerollContext.mutatorsStack.pushTransform(transform); final ui.Rect childPaintBounds = prerollChildren(prerollContext, childMatrix); - paintBounds = transform.transformRect(childPaintBounds); + (_filter as CkManagedSkImageFilterConvertible) + .imageFilter((SkImageFilter filter) { + paintBounds = + rectFromSkIRect(filter.getOutputBounds(toSkRect(childPaintBounds))); + }); prerollContext.mutatorsStack.pop(); } @@ -478,7 +484,7 @@ class PictureLayer extends Layer { @override void preroll(PrerollContext prerollContext, Matrix4 matrix) { - paintBounds = picture.cullRect!.shift(offset); + paintBounds = picture.cullRect.shift(offset); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index 576e1b2533..0f950f5a43 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -51,8 +51,6 @@ class LayerTree { final Iterable overlayCanvases = frame.viewEmbedder!.getOverlayCanvases(); overlayCanvases.forEach(internalNodesCanvas.addCanvas); - // Clear the canvases before painting - internalNodesCanvas.clear(const ui.Color(0x00000000)); final PaintContext context = PaintContext( internalNodesCanvas, frame.canvas, 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 374c24655d..52b8229ec2 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 @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; +import '../scene_painting.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; import 'image.dart'; @@ -14,18 +15,20 @@ import 'surface.dart'; import 'surface_factory.dart'; /// Implements [ui.Picture] on top of [SkPicture]. -class CkPicture implements ui.Picture { - CkPicture(SkPicture skPicture, this.cullRect) { +class CkPicture implements ScenePicture { + CkPicture(SkPicture skPicture) { _ref = UniqueRef(this, skPicture, 'Picture'); } late final UniqueRef _ref; - final ui.Rect? cullRect; SkPicture get skiaObject => _ref.nativeObject; @override - int get approximateBytesUsed => 0; + ui.Rect get cullRect => fromSkRect(skiaObject.cullRect()); + + @override + int get approximateBytesUsed => skiaObject.approximateBytesUsed(); @override bool get debugDisposed { @@ -39,7 +42,8 @@ class CkPicture implements ui.Picture { return result!; } - throw StateError('Picture.debugDisposed is only available when asserts are enabled.'); + throw StateError( + 'Picture.debugDisposed is only available when asserts are enabled.'); } /// This is set to true when [dispose] is called and is never reset back to @@ -96,8 +100,8 @@ class CkPicture implements ui.Picture { assert(debugCheckNotDisposed('Cannot convert picture to image.')); final Surface surface = SurfaceFactory.instance.pictureToImageSurface; - final CkSurface ckSurface = - surface.createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble())); + final CkSurface ckSurface = surface + .createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble())); final CkCanvas ckCanvas = ckSurface.getCanvas(); ckCanvas.clear(const ui.Color(0x00000000)); ckCanvas.drawPicture(this); @@ -110,7 +114,8 @@ class CkPicture implements ui.Picture { height: height.toDouble(), ); final Uint8List pixels = skImage.readPixels(0, 0, imageInfo); - final SkImage? rasterImage = canvasKit.MakeImage(imageInfo, pixels, (4 * width).toDouble()); + final SkImage? rasterImage = + canvasKit.MakeImage(imageInfo, pixels, (4 * width).toDouble()); if (rasterImage == null) { throw StateError('Unable to convert image pixels into SkImage.'); } 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 becc3996d0..7930d067b2 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 @@ -11,12 +11,10 @@ import 'canvaskit_api.dart'; import 'picture.dart'; class CkPictureRecorder implements ui.PictureRecorder { - ui.Rect? _cullRect; SkPictureRecorder? _skRecorder; CkCanvas? _recordingCanvas; CkCanvas beginRecording(ui.Rect bounds) { - _cullRect = bounds; final SkPictureRecorder recorder = _skRecorder = SkPictureRecorder(); final Float32List skRect = toSkRect(bounds); final SkCanvas skCanvas = recorder.beginRecording(skRect); @@ -36,7 +34,7 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPicture skPicture = recorder.finishRecordingAsPicture(); recorder.delete(); _skRecorder = null; - final CkPicture result = CkPicture(skPicture, _cullRect); + final CkPicture result = CkPicture(skPicture); // 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/rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index b8b420bb82..67f01fff85 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -33,6 +33,7 @@ class Rasterizer { SurfaceFactory.instance.baseSurface.acquireFrame(layerTree.frameSize); HtmlViewEmbedder.instance.frameSize = layerTree.frameSize; final CkCanvas canvas = frame.skiaCanvas; + canvas.clear(const ui.Color(0x00000000)); final Frame compositorFrame = context.acquireFrame(canvas, HtmlViewEmbedder.instance); 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 220f78dc4b..7cc6081747 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 @@ -51,6 +51,7 @@ void testMain() { _matrix4x4CompositionTests(); _toSkRectTests(); _skVerticesTests(); + _pictureTests(); group('SkParagraph', () { _paragraphTests(); }); @@ -1049,6 +1050,26 @@ void _skVerticesTests() { }); } +void _pictureTests() { + late SkPicture picture; + + setUp(() { + final SkPictureRecorder recorder = SkPictureRecorder(); + final SkCanvas canvas = recorder.beginRecording(toSkRect(ui.Rect.largest)); + canvas.drawRect(toSkRect(const ui.Rect.fromLTRB(20, 30, 40, 50)), + SkPaint()..setColorInt(0xffff00ff)); + picture = recorder.finishRecordingAsPicture(); + }); + test('cullRect', () { + expect( + fromSkRect(picture.cullRect()), const ui.Rect.fromLTRB(20, 30, 40, 50)); + }); + + test('approximateBytesUsed', () { + expect(picture.approximateBytesUsed() > 0, isTrue); + }); +} + void _canvasTests() { late SkPictureRecorder recorder; late SkCanvas canvas; @@ -1445,7 +1466,7 @@ void _canvasTests() { SkPaint()..setColorInt(0xAAFFFFFF), ); final CkPicture picture = - CkPicture(otherRecorder.finishRecordingAsPicture(), null); + CkPicture(otherRecorder.finishRecordingAsPicture()); 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/picture_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/picture_test.dart index ca467d556e..035ea3ba4c 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 @@ -44,11 +44,11 @@ void testMain() { expect(actualError, isNotNull); // TODO(yjbanov): cannot test precise message due to https://github.com/flutter/flutter/issues/96298 - expect('$actualError', startsWith( - 'Bad state: Test.\n' - 'The picture has been disposed. ' - 'When the picture was disposed the stack trace was:\n' - )); + expect( + '$actualError', + startsWith('Bad state: Test.\n' + 'The picture has been disposed. ' + 'When the picture was disposed the stack trace was:\n')); }); }); @@ -68,6 +68,76 @@ void testMain() { expect(data!.lengthInBytes, 10 * 15 * 4); expect(data.buffer.asUint32List().first, color.value); }); - // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 + + test('cullRect bounds are tight', () async { + const ui.Color red = ui.Color.fromRGBO(255, 0, 0, 1); + const ui.Color green = ui.Color.fromRGBO(0, 255, 0, 1); + const ui.Color blue = ui.Color.fromRGBO(0, 0, 255, 1); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawRRect( + ui.RRect.fromRectXY(const ui.Rect.fromLTRB(20, 20, 150, 300), 15, 15), + ui.Paint()..color = red, + ); + canvas.drawCircle( + const ui.Offset(200, 200), + 100, + ui.Paint()..color = green, + ); + canvas.drawOval( + const ui.Rect.fromLTRB(210, 40, 268, 199), + ui.Paint()..color = blue, + ); + + final CkPicture picture = recorder.endRecording() as CkPicture; + final ui.Rect bounds = picture.cullRect; + // Top left bounded by the red rrect, right bounded by right edge + // of red rrect, bottom bounded by bottom of green circle. + expect(bounds, equals(const ui.Rect.fromLTRB(20, 20, 300, 300))); + }); + + test('cullRect bounds with infinite size draw', () async { + const ui.Color red = ui.Color.fromRGBO(255, 0, 0, 1); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawColor(red, ui.BlendMode.src); + + final CkPicture picture = recorder.endRecording() as CkPicture; + final ui.Rect bounds = picture.cullRect; + // Since the drawColor command fills the entire canvas, the computed + // bounds default to the cullRect that is passed in when the + // PictureRecorder is created, ie ui.Rect.largest. + expect(bounds, equals(ui.Rect.largest)); + }); + + test('approximateBytesUsed', () async { + const ui.Color red = ui.Color.fromRGBO(255, 0, 0, 1); + const ui.Color green = ui.Color.fromRGBO(0, 255, 0, 1); + const ui.Color blue = ui.Color.fromRGBO(0, 0, 255, 1); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawRRect( + ui.RRect.fromRectXY(const ui.Rect.fromLTRB(20, 20, 150, 300), 15, 15), + ui.Paint()..color = red, + ); + canvas.drawCircle( + const ui.Offset(200, 200), + 100, + ui.Paint()..color = green, + ); + canvas.drawOval( + const ui.Rect.fromLTRB(210, 40, 268, 199), + ui.Paint()..color = blue, + ); + + final CkPicture picture = recorder.endRecording() as CkPicture; + final int bytesUsed = picture.approximateBytesUsed; + // Sanity check: the picture should use more than 20 bytes of memory. + expect(bytesUsed, greaterThan(20)); + }); + // TODO(hterkelsen): https://github.com/flutter/flutter/issues/60040 }, skip: isIosSafari); }