From 0424f1f86ea441d82b3fa01d6bfdf54eaf34c19d Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Thu, 29 Feb 2024 13:06:02 -0800 Subject: [PATCH] Reland "[skwasm] Clip pictures if they go beyond the bounds of the window." (flutter/engine#51077) This fixes https://github.com/flutter/flutter/issues/143800, where we are attempting to capture an image that is way too large. We only need to render the part of the image that will be visible in the window. This includes some additional fixes for regressions in the original fix. --- .../lib/web_ui/lib/src/engine/scene_view.dart | 42 ++++++++--- .../engine/skwasm/skwasm_impl/renderer.dart | 12 +++- .../web_ui/test/engine/scene_view_test.dart | 72 ++++++++++++++++++- 3 files changed, 116 insertions(+), 10 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart index b137b70f4f9..6f3031ae919 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart @@ -20,6 +20,7 @@ typedef RenderResult = ({ // composite pictures into the canvases in the DOM tree it builds. abstract class PictureRenderer { FutureOr renderPictures(List picture); + ScenePicture clipPicture(ScenePicture picture, ui.Rect clip); } class _SceneRender { @@ -43,15 +44,16 @@ class _SceneRender { // This class builds a DOM tree that composites an `EngineScene`. class EngineSceneView { - factory EngineSceneView(PictureRenderer pictureRenderer) { + factory EngineSceneView(PictureRenderer pictureRenderer, ui.FlutterView flutterView) { final DomElement sceneElement = createDomElement('flt-scene'); - return EngineSceneView._(pictureRenderer, sceneElement); + return EngineSceneView._(pictureRenderer, flutterView, sceneElement); } - EngineSceneView._(this.pictureRenderer, this.sceneElement); + EngineSceneView._(this.pictureRenderer, this.flutterView, this.sceneElement); final PictureRenderer pictureRenderer; final DomElement sceneElement; + final ui.FlutterView flutterView; List containers = []; @@ -87,11 +89,29 @@ class EngineSceneView { } Future _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async { + final ui.Rect screenBounds = ui.Rect.fromLTWH( + 0, + 0, + flutterView.physicalSize.width, + flutterView.physicalSize.height, + ); final List slices = scene.rootLayer.slices; final List picturesToRender = []; + final List originalPicturesToRender = []; for (final LayerSlice slice in slices) { if (slice is PictureSlice) { - picturesToRender.add(slice.picture); + final ui.Rect clippedRect = slice.picture.cullRect.intersect(screenBounds); + if (clippedRect.isEmpty) { + // This picture is completely offscreen, so don't render it at all + continue; + } else if (clippedRect == slice.picture.cullRect) { + // The picture doesn't need to be clipped, just render the original + originalPicturesToRender.add(slice.picture); + picturesToRender.add(slice.picture); + } else { + originalPicturesToRender.add(slice.picture); + picturesToRender.add(pictureRenderer.clipPicture(slice.picture, clippedRect)); + } } } final Map renderMap; @@ -99,7 +119,7 @@ class EngineSceneView { final RenderResult renderResult = await pictureRenderer.renderPictures(picturesToRender); renderMap = { for (int i = 0; i < picturesToRender.length; i++) - picturesToRender[i]: renderResult.imageBitmaps[i], + originalPicturesToRender[i]: renderResult.imageBitmaps[i], }; recorder?.recordRasterStart(renderResult.rasterStartMicros); recorder?.recordRasterFinish(renderResult.rasterEndMicros); @@ -115,6 +135,11 @@ class EngineSceneView { for (final LayerSlice slice in slices) { switch (slice) { case PictureSlice(): + final DomImageBitmap? bitmap = renderMap[slice.picture]; + if (bitmap == null) { + // We didn't render this slice because no part of it is visible. + continue; + } PictureSliceContainer? container; for (int j = 0; j < reusableContainers.length; j++) { final SliceContainer? candidate = reusableContainers[j]; @@ -125,13 +150,14 @@ class EngineSceneView { } } + final ui.Rect clippedBounds = slice.picture.cullRect.intersect(screenBounds); if (container != null) { - container.bounds = slice.picture.cullRect; + container.bounds = clippedBounds; } else { - container = PictureSliceContainer(slice.picture.cullRect); + container = PictureSliceContainer(clippedBounds); } container.updateContents(); - container.renderBitmap(renderMap[slice.picture]!); + container.renderBitmap(bitmap); newContainers.add(container); case PlatformViewSlice(): diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index a0bfd4780da..695c734cfed 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -413,7 +413,7 @@ class SkwasmRenderer implements Renderer { EngineSceneView _getSceneViewForView(EngineFlutterView view) { // TODO(mdebbar): Support multi-view mode. if (_sceneView == null) { - _sceneView = EngineSceneView(SkwasmPictureRenderer(surface)); + _sceneView = EngineSceneView(SkwasmPictureRenderer(surface), view); final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; implicitView.dom.setScene(_sceneView!.sceneElement); } @@ -482,4 +482,14 @@ class SkwasmPictureRenderer implements PictureRenderer { @override FutureOr renderPictures(List pictures) => surface.renderPictures(pictures.cast()); + + @override + ScenePicture clipPicture(ScenePicture picture, ui.Rect clip) { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, clip); + canvas.clipRect(clip); + canvas.drawPicture(picture); + + return recorder.endRecording() as ScenePicture; + } } diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart index 93d54b09b22..38c88f5049e 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart @@ -43,7 +43,60 @@ class StubPictureRenderer implements PictureRenderer { ); } + @override + ScenePicture clipPicture(ScenePicture picture, ui.Rect clip) { + clipRequests[picture] = clip; + return picture; + } + List renderedPictures = []; + Map clipRequests = {}; +} + +class StubFlutterView implements ui.FlutterView { + @override + double get devicePixelRatio => throw UnimplementedError(); + + @override + ui.Display get display => throw UnimplementedError(); + + @override + List get displayFeatures => throw UnimplementedError(); + + @override + ui.GestureSettings get gestureSettings => throw UnimplementedError(); + + @override + ui.ViewPadding get padding => throw UnimplementedError(); + + @override + ui.ViewConstraints get physicalConstraints => throw UnimplementedError(); + + @override + ui.Size get physicalSize => const ui.Size(1000, 1000); + + @override + ui.PlatformDispatcher get platformDispatcher => throw UnimplementedError(); + + @override + void render(ui.Scene scene, {ui.Size? size}) { + } + + @override + ui.ViewPadding get systemGestureInsets => throw UnimplementedError(); + + @override + void updateSemantics(ui.SemanticsUpdate update) { + } + + @override + int get viewId => throw UnimplementedError(); + + @override + ui.ViewPadding get viewInsets => throw UnimplementedError(); + + @override + ui.ViewPadding get viewPadding => throw UnimplementedError(); } void testMain() { @@ -56,7 +109,7 @@ void testMain() { setUp(() { stubPictureRenderer = StubPictureRenderer(); - sceneView = EngineSceneView(stubPictureRenderer); + sceneView = EngineSceneView(stubPictureRenderer, StubFlutterView()); }); test('SceneView places canvas according to device-pixel ratio', () async { @@ -149,4 +202,21 @@ void testMain() { expect(stubPictureRenderer.renderedPictures.first, pictures.first); expect(stubPictureRenderer.renderedPictures.last, pictures.last); }); + + test('SceneView clips pictures that are outside the window screen', () async { + final StubPicture picture = StubPicture(const ui.Rect.fromLTWH( + -50, + -50, + 100, + 120, + )); + + final EngineRootLayer rootLayer = EngineRootLayer(); + rootLayer.slices.add(PictureSlice(picture)); + final EngineScene scene = EngineScene(rootLayer); + await sceneView.renderScene(scene, null); + + expect(stubPictureRenderer.renderedPictures.length, 1); + expect(stubPictureRenderer.clipRequests.containsKey(picture), true); + }); }