forked from firka/flutter
[canvaskit] Enable CanvasKit to compute tight SkPicture bounds (flutter/engine#43361)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user