[canvaskit] Enable CanvasKit to compute tight SkPicture bounds (flutter/engine#43361)

This commit is contained in:
Harry Terkelsen
2023-08-01 16:07:19 -07:00
committed by GitHub
parent 3ff184cb6c
commit aed57ec7af
9 changed files with 145 additions and 29 deletions

View File

@@ -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()

View File

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

View File

@@ -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

View File

@@ -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,

View File

@@ -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<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 {
@@ -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.');
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();

View File

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