From d9bddde8ed4bc1f18976975efa8cf9123b2a8bd1 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 28 Feb 2024 16:12:57 -0800 Subject: [PATCH 1/2] Reland "[skwasm] Clip pictures if they go beyond the bounds of the window." --- lib/web_ui/lib/src/engine/scene_view.dart | 41 ++++++++++++++++--- .../engine/skwasm/skwasm_impl/renderer.dart | 10 +++++ lib/web_ui/test/engine/scene_view_test.dart | 24 +++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index b137b70f4f909..72a2f36fb44cf 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 { @@ -86,12 +87,41 @@ class EngineSceneView { } } + ScenePicture _clipPictureIfNeeded(ScenePicture picture, ui.Rect clip) { + final ui.Rect pictureRect = picture.cullRect; + if (pictureRect.left >= clip.left && + pictureRect.top >= clip.top && + pictureRect.right <= clip.right && + pictureRect.bottom <= clip.bottom) { + // The picture is already within the clip bounds. + return picture; + } + + return pictureRenderer.clipPicture(picture, clip); + } + + ui.Rect? _getScreenBounds() { + final DomScreen? screen = domWindow.screen; + if (screen == null) { + return null; + } + return ui.Rect.fromLTWH(0, 0, screen.width, screen.height); + } + Future _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async { + final ui.Rect? screenBounds = _getScreenBounds(); + if (screenBounds == null) { + // The browser isn't displaying the document. Skip rendering. + return; + } 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); + if (slice is PictureSlice && !slice.picture.cullRect.isEmpty) { + originalPicturesToRender.add(slice.picture); + final ScenePicture clippedPicture = _clipPictureIfNeeded(slice.picture, screenBounds); + picturesToRender.add(clippedPicture); } } final Map renderMap; @@ -99,7 +129,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); @@ -125,10 +155,11 @@ 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]!); 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..7eb3ef1d5e6ca 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 @@ -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..f9461162864ca 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -43,7 +43,14 @@ class StubPictureRenderer implements PictureRenderer { ); } + @override + ScenePicture clipPicture(ScenePicture picture, ui.Rect clip) { + clipRequests[picture] = clip; + return picture; + } + List renderedPictures = []; + Map clipRequests = {}; } void testMain() { @@ -149,4 +156,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); + }); } From 1d747667727472a61cdb1068cc38d41f6aa2a572 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 28 Feb 2024 16:45:05 -0800 Subject: [PATCH 2/2] Use flutterview for bounds and gracefully handle unrendered pictures. --- lib/web_ui/lib/src/engine/scene_view.dart | 63 +++++++++---------- .../engine/skwasm/skwasm_impl/renderer.dart | 2 +- lib/web_ui/test/engine/scene_view_test.dart | 48 +++++++++++++- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart index 72a2f36fb44cf..6f3031ae919df 100644 --- a/lib/web_ui/lib/src/engine/scene_view.dart +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -44,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,41 +88,30 @@ class EngineSceneView { } } - ScenePicture _clipPictureIfNeeded(ScenePicture picture, ui.Rect clip) { - final ui.Rect pictureRect = picture.cullRect; - if (pictureRect.left >= clip.left && - pictureRect.top >= clip.top && - pictureRect.right <= clip.right && - pictureRect.bottom <= clip.bottom) { - // The picture is already within the clip bounds. - return picture; - } - - return pictureRenderer.clipPicture(picture, clip); - } - - ui.Rect? _getScreenBounds() { - final DomScreen? screen = domWindow.screen; - if (screen == null) { - return null; - } - return ui.Rect.fromLTWH(0, 0, screen.width, screen.height); - } - Future _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async { - final ui.Rect? screenBounds = _getScreenBounds(); - if (screenBounds == null) { - // The browser isn't displaying the document. Skip rendering. - return; - } + 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 && !slice.picture.cullRect.isEmpty) { - originalPicturesToRender.add(slice.picture); - final ScenePicture clippedPicture = _clipPictureIfNeeded(slice.picture, screenBounds); - picturesToRender.add(clippedPicture); + if (slice is PictureSlice) { + 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; @@ -145,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]; @@ -162,7 +157,7 @@ class EngineSceneView { 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 7eb3ef1d5e6ca..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); } diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index f9461162864ca..38c88f5049e21 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -53,6 +53,52 @@ class StubPictureRenderer implements PictureRenderer { 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() { late EngineSceneView sceneView; late StubPictureRenderer stubPictureRenderer; @@ -63,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 {