Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Reland "[skwasm] Clip pictures if they go beyond the bounds of the window." #51077

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions lib/web_ui/lib/src/engine/scene_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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>[];

Expand Down Expand Up @@ -87,19 +89,37 @@ 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;
if (picturesToRender.isNotEmpty) {
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);
Expand All @@ -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];
Expand All @@ -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():
Expand Down
12 changes: 11 additions & 1 deletion lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
}
72 changes: 71 additions & 1 deletion lib/web_ui/test/engine/scene_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
});
}