Skip to content

Commit

Permalink
[canvaskit] Enable CanvasKit to compute tight SkPicture bounds (flutt…
Browse files Browse the repository at this point in the history
  • Loading branch information
Harry Terkelsen authored and gaaclarke committed Aug 30, 2023
1 parent 05ccd66 commit d5936b1
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 29 deletions.
20 changes: 18 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 5 additions & 4 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
}
Expand Down
12 changes: 9 additions & 3 deletions lib/web_ui/lib/src/engine/canvaskit/layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ class LayerTree {
final Iterable<CkCanvas> 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,
Expand Down
21 changes: 13 additions & 8 deletions lib/web_ui/lib/src/engine/canvaskit/picture.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<SkPicture>(this, skPicture, 'Picture');
}

late final UniqueRef<SkPicture> _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 {
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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.');
}
Expand Down
4 changes: 1 addition & 3 deletions lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
23 changes: 22 additions & 1 deletion lib/web_ui/test/canvaskit/canvaskit_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void testMain() {
_matrix4x4CompositionTests();
_toSkRectTests();
_skVerticesTests();
_pictureTests();
group('SkParagraph', () {
_paragraphTests();
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
82 changes: 76 additions & 6 deletions lib/web_ui/test/canvaskit/picture_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
});

Expand All @@ -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);
}

0 comments on commit d5936b1

Please sign in to comment.