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.
This commit is contained in:
Jackson Gardner 2024-02-29 13:06:02 -08:00 committed by GitHub
parent 0d3941630d
commit 0424f1f86e
3 changed files with 116 additions and 10 deletions

View File

@ -20,6 +20,7 @@ typedef RenderResult = ({
// composite pictures into the canvases in the DOM tree it builds.
abstract class PictureRenderer {
FutureOr<RenderResult> renderPictures(List<ScenePicture> 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<SliceContainer> containers = <SliceContainer>[];
@ -87,11 +89,29 @@ class EngineSceneView {
}
Future<void> _renderScene(EngineScene scene, FrameTimingRecorder? recorder) async {
final ui.Rect screenBounds = ui.Rect.fromLTWH(
0,
0,
flutterView.physicalSize.width,
flutterView.physicalSize.height,
);
final List<LayerSlice> slices = scene.rootLayer.slices;
final List<ScenePicture> picturesToRender = <ScenePicture>[];
final List<ScenePicture> originalPicturesToRender = <ScenePicture>[];
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<ScenePicture, DomImageBitmap> renderMap;
@ -99,7 +119,7 @@ class EngineSceneView {
final RenderResult renderResult = await pictureRenderer.renderPictures(picturesToRender);
renderMap = <ScenePicture, DomImageBitmap>{
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():

View File

@ -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<RenderResult> renderPictures(List<ScenePicture> pictures) =>
surface.renderPictures(pictures.cast<SkwasmPicture>());
@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;
}
}

View File

@ -43,7 +43,60 @@ class StubPictureRenderer implements PictureRenderer {
);
}
@override
ScenePicture clipPicture(ScenePicture picture, ui.Rect clip) {
clipRequests[picture] = clip;
return picture;
}
List<ScenePicture> renderedPictures = <ScenePicture>[];
Map<ScenePicture, ui.Rect> clipRequests = <ScenePicture, ui.Rect>{};
}
class StubFlutterView implements ui.FlutterView {
@override
double get devicePixelRatio => throw UnimplementedError();
@override
ui.Display get display => throw UnimplementedError();
@override
List<ui.DisplayFeature> 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);
});
}