diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index b137b70f4f909..6f3031ae919df 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/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/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index a0bfd4780da08..695c734cfed78 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/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/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 93d54b09b226f..38c88f5049e21 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/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); + }); }