From 111f5a45f00b9c03dc68e9bc451b4fa2d524d34a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Thu, 28 Aug 2025 08:27:58 -0700 Subject: [PATCH] [web] Refactor renderers to use the same frontend code (#174588) Refactors the renderer code so both renderers (Skwasm and CanvasKit) use the same SceneBuilder and platform view embedding code. This change is discussed in a design doc here: https://flutter.dev/go/web-renderer-unification Fixes https://github.com/flutter/flutter/issues/172311 Fixes https://github.com/flutter/flutter/issues/172308 Fixes https://github.com/flutter/flutter/issues/142072 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../flutter/lib/web_ui/lib/src/engine.dart | 6 +- .../lib/src/engine/canvaskit/image.dart | 11 - .../engine/canvaskit/picture_recorder.dart | 2 +- .../src/engine/canvaskit/raster_cache.dart | 52 - .../lib/src/engine/canvaskit/renderer.dart | 76 +- .../web_ui/lib/src/engine/frame_service.dart | 5 +- .../web_ui/lib/src/engine/image_decoder.dart | 11 + .../lib/web_ui/lib/src/engine/layers.dart | 1387 ----------------- .../lib/web_ui/lib/src/engine/lazy_path.dart | 4 +- .../web_ui/lib/src/engine/occlusion_map.dart | 89 ++ .../web_ui/lib/src/engine/platform_views.dart | 8 - .../lib/web_ui/lib/src/engine/renderer.dart | 48 +- .../web_ui/lib/src/engine/scene_builder.dart | 489 ------ .../web_ui/lib/src/engine/scene_painting.dart | 45 - .../lib/web_ui/lib/src/engine/scene_view.dart | 405 ----- .../lib/src/engine/skwasm/skwasm_impl.dart | 1 + .../src/engine/skwasm/skwasm_impl/canvas.dart | 14 +- .../engine/skwasm/skwasm_impl/filters.dart | 2 +- .../offscreen_canvas_rasterizer.dart | 83 + .../src/engine/skwasm/skwasm_impl/path.dart | 2 +- .../engine/skwasm/skwasm_impl/picture.dart | 4 +- .../skwasm/skwasm_impl/raw/raw_canvas.dart | 6 + .../skwasm/skwasm_impl/raw/raw_surface.dart | 6 + .../engine/skwasm/skwasm_impl/renderer.dart | 157 +- .../engine/skwasm/skwasm_impl/surface.dart | 10 + .../lib/web_ui/lib/src/engine/util.dart | 22 + .../src/flutter/lib/web_ui/skwasm/canvas.cpp | 9 + .../src/flutter/lib/web_ui/skwasm/surface.cpp | 10 + .../src/flutter/lib/web_ui/skwasm/surface.h | 1 + .../test/engine/scene_builder_test.dart | 380 ----- .../test/engine/scene_builder_utils.dart | 243 --- .../web_ui/test/engine/scene_view_test.dart | 413 ----- .../web_ui/test/ui/canvas_golden_test.dart | 4 +- .../test/ui/color_filter_golden_test.dart | 5 +- .../lib/web_ui/test/ui/filters_test.dart | 4 +- .../lib/web_ui/test/ui/layer_test.dart | 4 +- .../lib/web_ui/test/ui/multi_view_test.dart | 4 +- .../lib/web_ui/test/ui/picture_test.dart | 5 +- .../web_ui/test/ui/platform_view_test.dart | 50 +- 39 files changed, 354 insertions(+), 3723 deletions(-) delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/occlusion_map.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/platform_views.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart delete mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/offscreen_canvas_rasterizer.dart delete mode 100644 engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart delete mode 100644 engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart delete mode 100644 engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index dc774028779..25314ad53a8 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -74,7 +74,6 @@ export 'engine/layer/layer_scene_builder.dart'; export 'engine/layer/layer_tree.dart'; export 'engine/layer/layer_visitor.dart'; export 'engine/layer/n_way_canvas.dart'; -export 'engine/layers.dart'; export 'engine/lazy_path.dart'; export 'engine/mouse/context_menu.dart'; export 'engine/mouse/cursor.dart'; @@ -82,11 +81,11 @@ export 'engine/mouse/prevent_default.dart'; export 'engine/navigation/history.dart'; export 'engine/noto_font.dart'; export 'engine/noto_font_encoding.dart'; +export 'engine/occlusion_map.dart'; export 'engine/onscreen_logging.dart'; export 'engine/platform_dispatcher.dart'; export 'engine/platform_dispatcher/app_lifecycle_state.dart'; export 'engine/platform_dispatcher/view_focus_binding.dart'; -export 'engine/platform_views.dart'; export 'engine/platform_views/content_manager.dart'; export 'engine/platform_views/embedder.dart'; export 'engine/platform_views/message_handler.dart'; @@ -99,9 +98,6 @@ export 'engine/profiler.dart'; export 'engine/raw_keyboard.dart'; export 'engine/renderer.dart'; export 'engine/safe_browser_api.dart'; -export 'engine/scene_builder.dart'; -export 'engine/scene_painting.dart'; -export 'engine/scene_view.dart'; export 'engine/semantics/accessibility.dart'; export 'engine/semantics/alert.dart'; export 'engine/semantics/checkable.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart index 987271f188b..b1acf49d29f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -312,17 +312,6 @@ CkImage scaleImage(SkImage image, int? targetWidth, int? targetHeight) { return ckImage; } -/// Thrown when the web engine fails to decode an image, either due to a -/// network issue, corrupted image contents, or missing codec. -class ImageCodecException implements Exception { - ImageCodecException(this._message); - - final String _message; - - @override - String toString() => 'ImageCodecException: $_message'; -} - const String _kNetworkImageMessage = 'Failed to load network image.'; /// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart index f750deda057..ee9ef98d301 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -class CkPictureRecorder implements LayerPictureRecorder, ScenePictureRecorder { +class CkPictureRecorder implements LayerPictureRecorder { SkPictureRecorder? _skRecorder; CkCanvas? _recordingCanvas; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart deleted file mode 100644 index 192dd42c03d..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:ui/ui.dart' as ui; - -import '../vector_math.dart'; -import 'canvas.dart'; - -/// A cache of [Picture]s that have already been rasterized. -/// -/// In the case of a [Picture] with a lot of complex drawing commands, it can -/// be faster to rasterize that [Picture] into it's own canvas and composite -/// that canvas into the scene rather than replay the drawing commands for that -/// picture into the overall scene. -/// -/// This class is responsible for deciding if a [Picture] should be cached and -/// for creating the cached [Picture]s that can be drawn directly into the -/// canvas. -class RasterCache { - /// Make a decision on whether to cache [picture] under transform [matrix]. - /// - /// This is based on heuristics such as how many times [picture] has been - /// drawn before and the complexity of the drawing commands in [picture]. - /// - /// We also take into account the current transform [matrix], because, for - /// example, a picture may be rasterized with the identity transform, but - /// when it is used, the transform is a 3x scale. In this case, compositing - /// the rendered picture would result in pixelation. So, we must use both - /// the picture and the transform as a cache key. - /// - /// The flag [isComplex] is a hint to the raster cache that this picture - /// contains complex drawing commands, and as such should be more strongly - /// considered for caching. - /// - /// The flag [willChange] is a hint to the raster cache that this picture - /// will change, and so should be less likely to be cached. - void prepare(ui.Picture picture, Matrix4 matrix, bool isComplex, bool willChange) {} - - /// Gets a raster cache result for the [picture] at transform [matrix]. - RasterCacheResult get(ui.Picture picture, Matrix4 matrix) => RasterCacheResult(); -} - -/// A cache entry for a given picture and matrix. -class RasterCacheResult { - /// Whether or not this result represents a rasterized picture that can be - /// drawn into the scene. - bool get isValid => false; - - /// Draws the rasterized picture into the [canvas]. - void draw(CkCanvas canvas) {} -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 9d07f2efe11..2e259330505 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -428,71 +428,6 @@ class CanvasKitRenderer extends Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style) => isExperimentalWebParagraph ? WebParagraphBuilder(style) : CkParagraphBuilder(style); - // TODO(harryterkelsen): Merge this logic with the async logic in - // [EngineScene], https://github.com/flutter/flutter/issues/142072. - @override - Future renderScene(ui.Scene scene, EngineFlutterView view) async { - assert( - rasterizers.containsKey(view.viewId), - "Unable to render to a view which hasn't been registered", - ); - final ViewRasterizer rasterizer = rasterizers[view.viewId]!; - final RenderQueue renderQueue = rasterizer.queue; - final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled - ? FrameTimingRecorder() - : null; - if (renderQueue.current != null) { - // If a scene is already queued up, drop it and queue this one up instead - // so that the scene view always displays the most recently requested scene. - renderQueue.next?.completer.complete(); - final Completer completer = Completer(); - renderQueue.next = (scene: scene, completer: completer, recorder: recorder); - return completer.future; - } - final Completer completer = Completer(); - renderQueue.current = (scene: scene, completer: completer, recorder: recorder); - unawaited(_kickRenderLoop(rasterizer)); - return completer.future; - } - - Future _kickRenderLoop(ViewRasterizer rasterizer) async { - final RenderQueue renderQueue = rasterizer.queue; - final RenderRequest current = renderQueue.current!; - try { - await _renderScene(current.scene, rasterizer, current.recorder); - current.completer.complete(); - } catch (error, stackTrace) { - current.completer.completeError(error, stackTrace); - } - renderQueue.current = renderQueue.next; - renderQueue.next = null; - if (renderQueue.current == null) { - return; - } else { - return _kickRenderLoop(rasterizer); - } - } - - Future _renderScene( - ui.Scene scene, - ViewRasterizer rasterizer, - FrameTimingRecorder? recorder, - ) async { - // "Build finish" and "raster start" happen back-to-back because we - // render on the same thread, so there's no overhead from hopping to - // another thread. - // - // CanvasKit works differently from the HTML renderer in that in HTML - // we update the DOM in SceneBuilder.build, which is these function calls - // here are CanvasKit-only. - recorder?.recordBuildFinish(); - recorder?.recordRasterStart(); - - await rasterizer.draw((scene as LayerScene).layerTree, null); - recorder?.recordRasterFinish(); - recorder?.submitTimings(); - } - @override void clearFragmentProgramCache() { _programs.clear(); @@ -535,5 +470,14 @@ class CanvasKitRenderer extends Renderer { ); @override - void dumpDebugInfo() {} + void dumpDebugInfo() { + int i = 0; + for (final viewRasterizer in rasterizers.values) { + final Map? debugJson = viewRasterizer.dumpDebugInfo(); + if (debugJson != null) { + downloadDebugInfo('flutter-scene$i', debugJson); + i++; + } + } + } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart index 613e3a6b362..9e3639d6125 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart @@ -162,9 +162,8 @@ class FrameService { // In Flutter terminology "building a frame" consists of "beginning // frame" and "drawing frame". // - // We do not call `recordBuildFinish` from here because - // part of the rasterization process, particularly in the HTML - // renderer, takes place in the `SceneBuilder.build()`. + // We do not call `recordBuildFinish` from here because part of the + // rasterization process takes place in `SceneBuilder.build()`. FrameTimingRecorder.recordCurrentFrameBuildStart(); // We have to convert high-resolution time to `int` so we can construct diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart index e62646e2314..f67065b0092 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/image_decoder.dart @@ -267,3 +267,14 @@ ui.Image scaleImageIfNeeded( image.dispose(); return finalImage; } + +/// Thrown when the web engine fails to decode an image, either due to a +/// network issue, corrupted image contents, or missing codec. +class ImageCodecException implements Exception { + ImageCodecException(this._message); + + final String _message; + + @override + String toString() => 'ImageCodecException: $_message'; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart deleted file mode 100644 index e00924640b8..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart +++ /dev/null @@ -1,1387 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -class EngineRootLayer with PictureEngineLayer { - @override - final NoopOperation operation = const NoopOperation(); - - @override - EngineRootLayer emptyClone() => EngineRootLayer(); -} - -class NoopOperation implements LayerOperation { - const NoopOperation(); - - @override - PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'NoopOperation()'; - - @override - Map get debugJsonDescription { - return {'type': 'noop'}; - } -} - -class BackdropFilterLayer with PictureEngineLayer implements ui.BackdropFilterEngineLayer { - BackdropFilterLayer(this.operation); - - @override - final LayerOperation operation; - - @override - BackdropFilterLayer emptyClone() => BackdropFilterLayer(operation); -} - -class BackdropFilterOperation implements LayerOperation { - BackdropFilterOperation(this.filter, this.mode); - - final ui.ImageFilter filter; - final ui.BlendMode mode; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas) { - canvas.saveLayerWithFilter(ui.Rect.largest, ui.Paint()..blendMode = mode, filter); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); - - @override - bool get affectsBackdrop => true; - - @override - String toString() => 'BackdropFilterOperation(filter: $filter, mode: $mode)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'backdropFilter', - 'filter': filter.toString(), - 'mode': mode.toString(), - }; - } -} - -class ClipPathLayer with PictureEngineLayer implements ui.ClipPathEngineLayer { - ClipPathLayer(this.operation); - - @override - final ClipPathOperation operation; - - @override - ClipPathLayer emptyClone() => ClipPathLayer(operation); -} - -class ClipPathOperation implements LayerOperation { - ClipPathOperation(this.path, this.clip); - - final ScenePath path; - final ui.Clip clip; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(path.getBounds(), ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() { - return PlatformViewStyling(clip: PlatformViewPathClip(path)); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ClipPathOperation(path: $path, clip: $clip)'; - - @override - Map get debugJsonDescription { - final ui.Rect bounds = path.getBounds(); - return { - 'type': 'clipPath', - 'pathBounds': { - 'left': bounds.left, - 'top': bounds.top, - 'right': bounds.right, - 'bottom': bounds.bottom, - }, - 'clip': clip.name, - }; - } -} - -class ClipRectLayer with PictureEngineLayer implements ui.ClipRectEngineLayer { - ClipRectLayer(this.operation); - - @override - final ClipRectOperation operation; - - @override - ClipRectLayer emptyClone() => ClipRectLayer(operation); -} - -class ClipRectOperation implements LayerOperation { - const ClipRectOperation(this.rect, this.clip); - - final ui.Rect rect; - final ui.Clip clip; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rect); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(rect, ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() { - return PlatformViewStyling(clip: PlatformViewRectClip(rect)); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ClipRectOperation(rect: $rect, clip: $clip)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'clipRect', - 'rect': {'left': rect.left, 'top': rect.top, 'right': rect.right, 'bottom': rect.bottom}, - 'clip': clip.name, - }; - } -} - -class ClipRRectLayer with PictureEngineLayer implements ui.ClipRRectEngineLayer { - ClipRRectLayer(this.operation); - - @override - final ClipRRectOperation operation; - - @override - ClipRRectLayer emptyClone() => ClipRRectLayer(operation); -} - -class ClipRRectOperation implements LayerOperation { - const ClipRRectOperation(this.rrect, this.clip); - - final ui.RRect rrect; - final ui.Clip clip; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(rrect.outerRect, ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() { - return PlatformViewStyling(clip: PlatformViewRRectClip(rrect)); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ClipRRectOperation(rrect: $rrect, clip: $clip)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'clipRRect', - 'rrect': { - 'left': rrect.left, - 'top': rrect.top, - 'right': rrect.right, - 'bottom': rrect.bottom, - 'tlRadiusX': rrect.tlRadiusX, - 'tlRadiusY': rrect.tlRadiusY, - 'trRadiusX': rrect.trRadiusX, - 'trRadiusY': rrect.trRadiusY, - 'brRadiusX': rrect.brRadiusX, - 'brRadiusY': rrect.brRadiusY, - 'blRadiusX': rrect.blRadiusX, - 'blRadiusY': rrect.blRadiusY, - }, - 'clip': clip.name, - }; - } -} - -class ClipRSuperellipseLayer with PictureEngineLayer implements ui.ClipRSuperellipseEngineLayer { - ClipRSuperellipseLayer(this.operation); - - @override - final ClipRSuperellipseOperation operation; - - @override - ClipRSuperellipseLayer emptyClone() => ClipRSuperellipseLayer(operation); -} - -class ClipRSuperellipseOperation implements LayerOperation { - const ClipRSuperellipseOperation(this.rsuperellipse, this.clip); - - final ui.RSuperellipse rsuperellipse; - final ui.Clip clip; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.intersect(rsuperellipse.outerRect); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.clipRSuperellipse(rsuperellipse, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(rsuperellipse.outerRect, ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() { - // RSuperellipse ops in PlatformView are approximated by RRect because they - // are expensive. - return PlatformViewStyling(clip: PlatformViewRRectClip(rsuperellipse.toApproximateRRect())); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ClipRSuperellipseOperation(rsuperellipse: $rsuperellipse, clip: $clip)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'clipRSuperellipse', - 'rsuperellipse': { - 'left': rsuperellipse.left, - 'top': rsuperellipse.top, - 'right': rsuperellipse.right, - 'bottom': rsuperellipse.bottom, - 'tlRadiusX': rsuperellipse.tlRadiusX, - 'tlRadiusY': rsuperellipse.tlRadiusY, - 'trRadiusX': rsuperellipse.trRadiusX, - 'trRadiusY': rsuperellipse.trRadiusY, - 'brRadiusX': rsuperellipse.brRadiusX, - 'brRadiusY': rsuperellipse.brRadiusY, - 'blRadiusX': rsuperellipse.blRadiusX, - 'blRadiusY': rsuperellipse.blRadiusY, - }, - 'clip': clip.name, - }; - } -} - -class ColorFilterLayer with PictureEngineLayer implements ui.ColorFilterEngineLayer { - ColorFilterLayer(this.operation); - - @override - final ColorFilterOperation operation; - - @override - ColorFilterLayer emptyClone() => ColorFilterLayer(operation); -} - -class ColorFilterOperation implements LayerOperation { - ColorFilterOperation(this.filter); - - final ui.ColorFilter filter; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas) { - canvas.saveLayer(ui.Rect.largest, ui.Paint()..colorFilter = filter); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ColorFilterOperation(filter: $filter)'; - - @override - Map get debugJsonDescription { - return {'type': 'colorFilter', 'filter': filter.toString()}; - } -} - -class ImageFilterLayer with PictureEngineLayer implements ui.ImageFilterEngineLayer { - ImageFilterLayer(this.operation); - - @override - final ImageFilterOperation operation; - - @override - ImageFilterLayer emptyClone() => ImageFilterLayer(operation); -} - -class ImageFilterOperation implements LayerOperation { - ImageFilterOperation(this.filter, this.offset); - - final SceneImageFilter filter; - final ui.Offset offset; - - @override - ui.Rect mapRect(ui.Rect contentRect) => filter.filterBounds(contentRect).shift(offset); - - @override - void pre(SceneCanvas canvas) { - if (offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(offset.dx, offset.dy); - } - canvas.saveLayer(ui.Rect.largest, ui.Paint()..imageFilter = filter); - } - - @override - void post(SceneCanvas canvas) { - if (offset != ui.Offset.zero) { - canvas.restore(); - } - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() { - PlatformViewStyling styling = const PlatformViewStyling(); - if (offset != ui.Offset.zero) { - styling = PlatformViewStyling(position: PlatformViewPosition.offset(offset)); - } - final Matrix4? transform = filter.transform; - if (transform != null) { - styling = PlatformViewStyling.combine( - styling, - PlatformViewStyling(position: PlatformViewPosition.transform(transform)), - ); - } - return const PlatformViewStyling(); - } - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'ImageFilterOperation(filter: $filter)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'imageFilter', - 'filter': filter.toString(), - 'offset': {'x': offset.dx, 'y': offset.dy}, - }; - } -} - -class OffsetLayer with PictureEngineLayer implements ui.OffsetEngineLayer { - OffsetLayer(this.operation); - - @override - final OffsetOperation operation; - - @override - OffsetLayer emptyClone() => OffsetLayer(operation); -} - -class OffsetOperation implements LayerOperation { - OffsetOperation(this.dx, this.dy); - - final double dx; - final double dy; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.translate(dx, dy); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() => - PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(dx, dy))); - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'OffsetOperation(dx: $dx, dy: $dy)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'offset', - 'offset': {'x': dx, 'y': dy}, - }; - } -} - -class OpacityLayer with PictureEngineLayer implements ui.OpacityEngineLayer { - OpacityLayer(this.operation); - - @override - final OpacityOperation operation; - - @override - OpacityLayer emptyClone() => OpacityLayer(operation); -} - -class OpacityOperation implements LayerOperation { - OpacityOperation(this.alpha, this.offset); - - final int alpha; - final ui.Offset offset; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect.shift(offset); - - @override - void pre(SceneCanvas canvas) { - if (offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(offset.dx, offset.dy); - } - canvas.saveLayer(ui.Rect.largest, ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0)); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - if (offset != ui.Offset.zero) { - canvas.restore(); - } - } - - @override - PlatformViewStyling createPlatformViewStyling() => PlatformViewStyling( - position: offset != ui.Offset.zero - ? PlatformViewPosition.offset(offset) - : const PlatformViewPosition.zero(), - opacity: alpha.toDouble() / 255.0, - ); - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'OpacityOperation(offset: $offset, alpha: $alpha)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'opacity', - 'alpha': alpha, - 'offset': {'x': offset.dx, 'y': offset.dy}, - }; - } -} - -class TransformLayer with PictureEngineLayer implements ui.TransformEngineLayer { - TransformLayer(this.operation); - - @override - final TransformOperation operation; - - @override - TransformLayer emptyClone() => TransformLayer(operation); -} - -class TransformOperation implements LayerOperation { - TransformOperation(this.transform); - - final Float64List transform; - - Matrix4? _memoizedMatrix; - Matrix4 get matrix => - _memoizedMatrix ?? (_memoizedMatrix = Matrix4.fromFloat32List(toMatrix32(transform))); - - @override - ui.Rect mapRect(ui.Rect contentRect) => matrix.transformRect(contentRect); - - @override - void pre(SceneCanvas canvas) { - canvas.save(); - canvas.transform(transform); - } - - @override - void post(SceneCanvas canvas) { - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() => - PlatformViewStyling(position: PlatformViewPosition.transform(matrix)); - - @override - bool get affectsBackdrop => false; - - @override - String toString() => 'TransformOperation(matrix: $matrix)'; - - @override - Map get debugJsonDescription { - return {'type': 'transform', 'matrix': transform.toList()}; - } -} - -class ShaderMaskLayer with PictureEngineLayer implements ui.ShaderMaskEngineLayer { - ShaderMaskLayer(this.operation); - - @override - final ShaderMaskOperation operation; - - @override - ShaderMaskLayer emptyClone() => ShaderMaskLayer(operation); -} - -class ShaderMaskOperation implements LayerOperation { - ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); - - final ui.Shader shader; - final ui.Rect maskRect; - final ui.BlendMode blendMode; - - @override - ui.Rect mapRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas) { - canvas.saveLayer(ui.Rect.largest, ui.Paint()); - } - - @override - void post(SceneCanvas canvas) { - canvas.save(); - canvas.translate(maskRect.left, maskRect.top); - canvas.drawRect( - ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), - ui.Paint() - ..blendMode = blendMode - ..shader = shader, - ); - canvas.restore(); - canvas.restore(); - } - - @override - PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); - - @override - bool get affectsBackdrop => false; - - @override - String toString() => - 'ShaderMaskOperation(shader: $shader, maskRect: $maskRect, blendMode: $blendMode)'; - - @override - Map get debugJsonDescription { - return { - 'type': 'shaderMask', - 'shader': shader.toString(), - 'maskRect': { - 'left': maskRect.left, - 'top': maskRect.top, - 'right': maskRect.right, - 'bottom': maskRect.bottom, - }, - }; - } -} - -class PlatformView { - PlatformView(this.viewId, this.bounds, this.styling); - - final int viewId; - - // The bounds of this platform view, in the layer's local coordinate space. - final ui.Rect bounds; - - final PlatformViewStyling styling; - - @override - String toString() { - return 'PlatformView(viewId: $viewId, bounds: $bounds, styling: $styling)'; - } -} - -class LayerSlice { - LayerSlice(this.picture, this.platformViews); - - // The picture of native flutter content to be rendered - ScenePicture picture; - - // Platform views to be placed on top of the flutter content. - final List platformViews; - - void dispose() { - picture.dispose(); - } -} - -mixin PictureEngineLayer implements ui.EngineLayer { - // Each layer is represented as a series of "slices" which contain flutter content - // with platform views on top. This is ordered from bottommost to topmost. - List slices = []; - - List drawCommands = []; - PlatformViewStyling platformViewStyling = const PlatformViewStyling(); - - LayerOperation get operation; - - PictureEngineLayer emptyClone(); - - @override - void dispose() { - for (final LayerSlice? slice in slices) { - slice?.dispose(); - } - } - - @override - String toString() { - return 'PictureEngineLayer($operation)'; - } - - Map get debugJsonDescription { - return { - 'operation': operation.debugJsonDescription, - 'commands': drawCommands.map((c) => c.debugJsonDescription).toList(), - }; - } - - bool get isSimple { - if (slices.length > 1) { - return false; - } - final LayerSlice? singleSlice = slices.firstOrNull; - if (singleSlice == null || singleSlice.platformViews.isEmpty) { - return true; - } - return false; - } -} - -abstract class LayerOperation { - const LayerOperation(); - - // Given an input content rectangle, this returns a conservative estimate of - // the covering rectangle of the content after it has been processed by the - // layer operation. - ui.Rect mapRect(ui.Rect contentRect); - - void pre(SceneCanvas canvas); - void post(SceneCanvas canvas); - - PlatformViewStyling createPlatformViewStyling(); - - /// Indicates whether this operation's `pre` and `post` methods should be - /// invoked even if it contains no pictures. (Most operations don't need to - /// actually be performed at all if they don't contain any pictures.) - bool get affectsBackdrop; - - Map get debugJsonDescription; -} - -sealed class LayerDrawCommand { - Map get debugJsonDescription; -} - -class PictureDrawCommand extends LayerDrawCommand { - PictureDrawCommand(this.offset, this.picture, this.sliceIndex); - - final int sliceIndex; - final ui.Offset offset; - final ScenePicture picture; - - @override - Map get debugJsonDescription { - final bounds = picture.cullRect; - return { - 'type': 'picture', - 'sliceIndex': sliceIndex, - 'offset': {'x': offset.dx, 'y': offset.dy}, - 'localBounds': { - 'left': bounds.left, - 'top': bounds.top, - 'right': bounds.right, - 'bottom': bounds.bottom, - }, - }; - } -} - -class PlatformViewDrawCommand extends LayerDrawCommand { - PlatformViewDrawCommand(this.viewId, this.bounds, this.sliceIndex); - - final int sliceIndex; - final int viewId; - final ui.Rect bounds; - - @override - Map get debugJsonDescription { - return { - 'type': 'platformView', - 'sliceIndex': sliceIndex, - 'viewId': viewId, - 'localBounds': { - 'left': bounds.left, - 'top': bounds.top, - 'right': bounds.right, - 'bottom': bounds.bottom, - }, - }; - } -} - -class RetainedLayerDrawCommand extends LayerDrawCommand { - RetainedLayerDrawCommand(this.layer); - - final PictureEngineLayer layer; - - @override - Map get debugJsonDescription { - return {'type': 'layer', 'layer': layer.debugJsonDescription}; - } -} - -// Represents how a platform view should be positioned in the scene. -// This object is immutable, so it can be reused across different platform -// views that have the same positioning. -class PlatformViewPosition { - // No transformation at all. We leave both fields null. - const PlatformViewPosition.zero() : offset = null, transform = null; - - // A simple offset is the most common scenario. In those cases, we only - // store the offset and leave the transform as null - const PlatformViewPosition.offset(this.offset) : transform = null; - - // In more complex cases, we store the transform. In those cases, the offset - // is left as null. - const PlatformViewPosition.transform(this.transform) : offset = null; - - bool get isZero => (offset == null) && (transform == null); - - ui.Rect mapLocalToGlobal(ui.Rect rect) { - if (offset != null) { - return rect.shift(offset!); - } - if (transform != null) { - return transform!.transformRect(rect); - } - return rect; - } - - // Note that by construction only one of these can be set at any given time, not both. - final ui.Offset? offset; - final Matrix4? transform; - - static PlatformViewPosition combine(PlatformViewPosition outer, PlatformViewPosition inner) { - // We try to reuse existing objects if possible, if they are immutable. - if (outer.isZero) { - return inner; - } - if (inner.isZero) { - return outer; - } - final ui.Offset? outerOffset = outer.offset; - final ui.Offset? innerOffset = inner.offset; - if (outerOffset != null && innerOffset != null) { - // Both positions are simple offsets, so they can be combined cheaply - // into another offset. - return PlatformViewPosition.offset(outerOffset + innerOffset); - } - - // Otherwise, at least one of the positions involves a matrix transform. - final Matrix4 innerTransform; - final Matrix4 outerTransform; - if (innerOffset != null) { - innerTransform = Matrix4.translationValues(innerOffset.dx, innerOffset.dy, 0); - } else { - innerTransform = inner.transform!; - } - if (outerOffset != null) { - outerTransform = Matrix4.translationValues(outerOffset.dx, outerOffset.dy, 0); - } else { - outerTransform = outer.transform!; - } - return PlatformViewPosition.transform(outerTransform.multiplied(innerTransform)); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! PlatformViewPosition) { - return false; - } - return (offset == other.offset) && (transform == other.transform); - } - - @override - int get hashCode { - return Object.hash(offset, transform); - } - - @override - String toString() { - if (offset != null) { - return 'PlatformViewPosition(offset: $offset)'; - } - if (transform != null) { - return 'PlatformViewPosition(transform: $transform)'; - } - return 'PlatformViewPosition(zero)'; - } -} - -// Represents the styling to be performed on a platform view when it is -// composited. This object is immutable so that it can be reused with different -// platform views that have the same styling. -class PlatformViewStyling { - const PlatformViewStyling({ - this.position = const PlatformViewPosition.zero(), - this.clip = const PlatformViewNoClip(), - this.opacity = 1.0, - }); - - bool get isDefault => position.isZero && (opacity == 1.0) && clip is PlatformViewNoClip; - - final PlatformViewPosition position; - final double opacity; - final PlatformViewClip clip; - - ui.Rect mapLocalToGlobal(ui.Rect rect) { - return position.mapLocalToGlobal(rect).intersect(clip.outerRect); - } - - static PlatformViewStyling combine(PlatformViewStyling outer, PlatformViewStyling inner) { - // Attempt to reuse one of the existing immutable objects. - if (outer.isDefault) { - return inner; - } - if (inner.isDefault) { - return outer; - } - return PlatformViewStyling( - position: PlatformViewPosition.combine(outer.position, inner.position), - clip: PlatformViewClip.combine(outer.clip, inner.clip.positioned(outer.position)), - opacity: outer.opacity * inner.opacity, - ); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! PlatformViewStyling) { - return false; - } - return (position == other.position) && (opacity == other.opacity) && (clip == other.clip); - } - - @override - int get hashCode { - return Object.hash(position, opacity, clip); - } - - @override - String toString() { - return 'PlatformViewStyling(position: $position, clip: $clip, opacity: $opacity)'; - } -} - -sealed class PlatformViewClip { - PlatformViewClip positioned(PlatformViewPosition position); - - /// The largest rectangle that is entirely inside the clip region. All - /// inside of this region is unclipped. - ui.Rect get innerRect; - - /// The bounding rectangle of the clip region. All content outside of this - /// region is clipped. - ui.Rect get outerRect; - - ScenePath get toPath; - - static bool rectCovers(ui.Rect covering, ui.Rect covered) { - return covering.left <= covered.left && - covering.right >= covered.right && - covering.top <= covered.top && - covering.bottom >= covered.bottom; - } - - static PlatformViewClip combine(PlatformViewClip outer, PlatformViewClip inner) { - if (outer is PlatformViewNoClip) { - return inner; - } - if (inner is PlatformViewNoClip) { - return outer; - } - - if (rectCovers(outer.innerRect, inner.outerRect)) { - return inner; - } - - if (rectCovers(inner.innerRect, outer.outerRect)) { - return outer; - } - - return PlatformViewPathClip( - ui.Path.combine(ui.PathOperation.intersect, outer.toPath, inner.toPath) as ScenePath, - ); - } -} - -class PlatformViewNoClip implements PlatformViewClip { - const PlatformViewNoClip(); - - @override - PlatformViewClip positioned(PlatformViewPosition positioned) { - return this; - } - - @override - ScenePath get toPath => ui.Path() as ScenePath; - - @override - bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == PlatformViewNoClip); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - ui.Rect get innerRect => ui.Rect.zero; - - @override - ui.Rect get outerRect => ui.Rect.largest; - - @override - String toString() { - return 'PlatformViewClip(none)'; - } -} - -class PlatformViewRectClip implements PlatformViewClip { - const PlatformViewRectClip(this.rect); - - final ui.Rect rect; - - @override - PlatformViewClip positioned(PlatformViewPosition position) { - if (position.isZero) { - return this; - } - final ui.Offset? offset = position.offset; - if (offset != null) { - return PlatformViewRectClip(rect.shift(offset)); - } else { - return PlatformViewPathClip(toPath.transform(position.transform!.toFloat64()) as ScenePath); - } - } - - @override - ScenePath get toPath => (ui.Path() as ScenePath)..addRect(rect); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is PlatformViewRectClip && rect == other.rect; - } - - @override - int get hashCode => Object.hash(runtimeType, rect); - - @override - ui.Rect get innerRect => rect; - - @override - ui.Rect get outerRect => rect; - - @override - String toString() { - return 'PlatformViewRectClip($rect)'; - } -} - -class PlatformViewRRectClip implements PlatformViewClip { - const PlatformViewRRectClip(this.rrect); - - final ui.RRect rrect; - - @override - PlatformViewClip positioned(PlatformViewPosition position) { - if (position.isZero) { - return this; - } - final ui.Offset? offset = position.offset; - if (offset != null) { - return PlatformViewRRectClip(rrect.shift(offset)); - } else { - return PlatformViewPathClip(toPath.transform(position.transform!.toFloat64()) as ScenePath); - } - } - - @override - ScenePath get toPath => (ui.Path() as ScenePath)..addRRect(rrect); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is PlatformViewRRectClip && rrect == other.rrect; - } - - @override - int get hashCode => Object.hash(runtimeType, rrect); - - @override - ui.Rect get innerRect => rrect.safeInnerRect; - - @override - ui.Rect get outerRect => rrect.outerRect; - - @override - String toString() { - return 'PlatformViewRRectClip($rrect)'; - } -} - -class PlatformViewPathClip implements PlatformViewClip { - PlatformViewPathClip(this.path); - - final ScenePath path; - - @override - PlatformViewClip positioned(PlatformViewPosition position) { - if (position.isZero) { - return this; - } - - final ui.Offset? offset = position.offset; - if (offset != null) { - return PlatformViewPathClip(path.shift(offset) as ScenePath); - } else { - return PlatformViewPathClip(path.transform(position.transform!.toFloat64()) as ScenePath); - } - } - - @override - ScenePath get toPath => path; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - return other is PlatformViewPathClip && path == other.path; - } - - @override - int get hashCode => Object.hash(runtimeType, path); - - @override - ui.Rect get innerRect => ui.Rect.zero; - - @override - ui.Rect get outerRect => path.getBounds(); - - @override - String toString() { - return 'PlatformViewPathClip($path)'; - } -} - -class LayerSliceBuilder { - @visibleForTesting - static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; - - static (ui.PictureRecorder, SceneCanvas) defaultRecorderFactory(ui.Rect rect) { - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; - return (recorder, canvas); - } - - void addPicture(ui.Offset offset, ScenePicture picture) { - pictures.add((picture, offset)); - final ui.Rect pictureRect = picture.cullRect.shift(offset); - cullRect = cullRect?.expandToInclude(pictureRect) ?? pictureRect; - } - - (ui.PictureRecorder, SceneCanvas) createRecorder(ui.Rect rect) => - debugRecorderFactory != null ? debugRecorderFactory!(rect) : defaultRecorderFactory(rect); - - LayerSlice buildWithOperation(LayerOperation operation, ui.Rect? backdropRect) { - final ui.Rect effectiveRect; - if (backdropRect != null && cullRect != null) { - effectiveRect = cullRect!.expandToInclude(backdropRect); - } else { - effectiveRect = backdropRect ?? cullRect ?? ui.Rect.zero; - } - final (recorder, canvas) = createRecorder(operation.mapRect(effectiveRect)); - operation.pre(canvas); - for (final (picture, offset) in pictures) { - if (offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(offset.dx, offset.dy); - canvas.drawPicture(picture); - canvas.restore(); - } else { - canvas.drawPicture(picture); - } - } - operation.post(canvas); - final ui.Picture picture = recorder.endRecording(); - return LayerSlice(picture as ScenePicture, platformViews); - } - - final List<(ScenePicture, ui.Offset)> pictures = []; - ui.Rect? cullRect; - final List platformViews = []; -} - -class LayerBuilder { - factory LayerBuilder.rootLayer() { - return LayerBuilder._(null, EngineRootLayer()); - } - - factory LayerBuilder.childLayer({ - required LayerBuilder parent, - required PictureEngineLayer layer, - }) { - return LayerBuilder._(parent, layer); - } - - LayerBuilder._(this.parent, this.layer); - - final LayerBuilder? parent; - final PictureEngineLayer layer; - - final List sliceBuilders = []; - final List drawCommands = []; - - ui.Rect? getCurrentBackdropRectAtSliceIndex(int index) { - final parentRect = parent?.getCurrentBackdropRectAtSliceIndex(index); - final sliceBuilder = index < sliceBuilders.length ? sliceBuilders[index] : null; - final sliceRect = sliceBuilder?.cullRect; - final ui.Rect? combinedRect; - if (sliceRect != null && parentRect != null) { - combinedRect = parentRect.expandToInclude(sliceRect); - } else { - combinedRect = parentRect ?? sliceRect; - } - return combinedRect == null ? null : layer.operation.mapRect(combinedRect); - } - - int getCurrentSliceCount() { - final parentSliceCount = parent?.getCurrentSliceCount(); - if (parentSliceCount != null) { - return math.max(parentSliceCount, sliceBuilders.length); - } else { - return sliceBuilders.length; - } - } - - PlatformViewStyling? _memoizedPlatformViewStyling; - PlatformViewStyling get platformViewStyling { - return _memoizedPlatformViewStyling ??= layer.operation.createPlatformViewStyling(); - } - - PlatformViewStyling? _memoizedGlobalPlatformViewStyling; - PlatformViewStyling get globalPlatformViewStyling { - if (_memoizedGlobalPlatformViewStyling != null) { - return _memoizedGlobalPlatformViewStyling!; - } - if (parent != null) { - return _memoizedGlobalPlatformViewStyling ??= PlatformViewStyling.combine( - parent!.globalPlatformViewStyling, - platformViewStyling, - ); - } - return _memoizedGlobalPlatformViewStyling ??= platformViewStyling; - } - - LayerSliceBuilder getOrCreateSliceBuilderAtIndex(int index) { - while (sliceBuilders.length <= index) { - sliceBuilders.add(null); - } - final LayerSliceBuilder? existingSliceBuilder = sliceBuilders[index]; - if (existingSliceBuilder != null) { - return existingSliceBuilder; - } - final LayerSliceBuilder newSliceBuilder = LayerSliceBuilder(); - sliceBuilders[index] = newSliceBuilder; - return newSliceBuilder; - } - - void addPicture(ui.Offset offset, ui.Picture picture, {required int sliceIndex}) { - final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); - sliceBuilder.addPicture(offset, picture as ScenePicture); - drawCommands.add(PictureDrawCommand(offset, picture, sliceIndex)); - } - - void addPlatformView(int viewId, {required ui.Rect bounds, required int sliceIndex}) { - final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(sliceIndex); - sliceBuilder.platformViews.add(PlatformView(viewId, bounds, platformViewStyling)); - drawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); - } - - void mergeLayer(PictureEngineLayer layer) { - for (int i = 0; i < layer.slices.length; i++) { - final LayerSlice? slice = layer.slices[i]; - if (slice != null) { - final LayerSliceBuilder sliceBuilder = getOrCreateSliceBuilderAtIndex(i); - sliceBuilder.addPicture(ui.Offset.zero, slice.picture); - sliceBuilder.platformViews.addAll( - slice.platformViews.map((PlatformView view) { - return PlatformView( - view.viewId, - view.bounds, - PlatformViewStyling.combine(platformViewStyling, view.styling), - ); - }), - ); - } - } - drawCommands.add(RetainedLayerDrawCommand(layer)); - } - - PictureEngineLayer sliceUp() { - final int sliceCount = layer.operation.affectsBackdrop - ? getCurrentSliceCount() - : sliceBuilders.length; - final slices = []; - for (int i = 0; i < sliceCount; i++) { - final ui.Rect? backdropRect; - if (layer.operation.affectsBackdrop) { - backdropRect = getCurrentBackdropRectAtSliceIndex(i); - } else { - backdropRect = null; - } - final LayerSliceBuilder? builder; - if (backdropRect != null) { - builder = getOrCreateSliceBuilderAtIndex(i); - } else { - builder = i < sliceBuilders.length ? sliceBuilders[i] : null; - } - slices.add(builder?.buildWithOperation(layer.operation, backdropRect)); - } - layer.slices = slices; - return layer; - } - - PictureEngineLayer build() { - layer.drawCommands = drawCommands; - layer.platformViewStyling = platformViewStyling; - return sliceUp(); - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart index b662f147bac..140f9654e3b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart @@ -8,7 +8,7 @@ import 'dart:typed_data'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -abstract class DisposablePath implements ScenePath, LayerPath { +abstract class DisposablePath implements LayerPath { @override DisposablePathMetrics computeMetrics({bool forceClosed = false}); @@ -355,7 +355,7 @@ abstract class DisposablePathConstructors { ); } -class LazyPath implements ScenePath, LayerPath, Collectable { +class LazyPath implements LayerPath, Collectable { factory LazyPath(DisposablePathConstructors constructors) => LazyPath._(constructors, ui.PathFillType.nonZero, () => constructors.createNew()); LazyPath._(this.constructors, this._fillType, this.initializer) : _commands = []; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/occlusion_map.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/occlusion_map.dart new file mode 100644 index 00000000000..49b4b08ca46 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/occlusion_map.dart @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:ui/ui.dart' as ui; + +sealed class OcclusionMapNode { + bool overlaps(ui.Rect rect); + OcclusionMapNode insert(ui.Rect rect); + ui.Rect get boundingBox; +} + +class OcclusionMapEmpty implements OcclusionMapNode { + @override + ui.Rect get boundingBox => ui.Rect.zero; + + @override + OcclusionMapNode insert(ui.Rect rect) => OcclusionMapLeaf(rect); + + @override + bool overlaps(ui.Rect rect) => false; +} + +class OcclusionMapLeaf implements OcclusionMapNode { + OcclusionMapLeaf(this.rect); + + final ui.Rect rect; + + @override + ui.Rect get boundingBox => rect; + + @override + OcclusionMapNode insert(ui.Rect other) => OcclusionMapBranch(this, OcclusionMapLeaf(other)); + + @override + bool overlaps(ui.Rect other) => rect.overlaps(other); +} + +class OcclusionMapBranch implements OcclusionMapNode { + OcclusionMapBranch(this.left, this.right) + : boundingBox = left.boundingBox.expandToInclude(right.boundingBox); + + final OcclusionMapNode left; + final OcclusionMapNode right; + + @override + final ui.Rect boundingBox; + + double _areaOfUnion(ui.Rect first, ui.Rect second) { + return (math.max(first.right, second.right) - math.min(first.left, second.left)) * + (math.max(first.bottom, second.bottom) - math.min(first.top, second.top)); + } + + @override + OcclusionMapNode insert(ui.Rect other) { + // Try to create nodes with the smallest possible area + final double leftOtherArea = _areaOfUnion(left.boundingBox, other); + final double rightOtherArea = _areaOfUnion(right.boundingBox, other); + final double leftRightArea = boundingBox.width * boundingBox.height; + if (leftOtherArea < rightOtherArea) { + if (leftOtherArea < leftRightArea) { + return OcclusionMapBranch(left.insert(other), right); + } + } else { + if (rightOtherArea < leftRightArea) { + return OcclusionMapBranch(left, right.insert(other)); + } + } + return OcclusionMapBranch(this, OcclusionMapLeaf(other)); + } + + @override + bool overlaps(ui.Rect rect) { + if (!boundingBox.overlaps(rect)) { + return false; + } + return left.overlaps(rect) || right.overlaps(rect); + } +} + +class OcclusionMap { + OcclusionMapNode root = OcclusionMapEmpty(); + + void addRect(ui.Rect rect) => root = root.insert(rect); + + bool overlaps(ui.Rect rect) => root.overlaps(rect); +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views.dart deleted file mode 100644 index 956e19d8e04..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// TODO(yjbanov): The code in this file was temporarily moved to lib/web_ui/lib/ui.dart -// during the NNBD migration so that `dart:ui` does not have to export -// `dart:_engine`. NNBD does not allow exported non-migrated libraries -// from migrated libraries. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart index dca456f3571..3916e8ad28a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/renderer.dart @@ -275,7 +275,53 @@ abstract class Renderer { /// Map from view id to the associated [ViewRasterizer] for that view. final Map rasterizers = {}; - Future renderScene(ui.Scene scene, EngineFlutterView view); + Future renderScene(ui.Scene scene, EngineFlutterView view) async { + assert( + rasterizers.containsKey(view.viewId), + "Unable to render to a view which hasn't been registered", + ); + final ViewRasterizer rasterizer = rasterizers[view.viewId]!; + final RenderQueue renderQueue = rasterizer.queue; + final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled + ? FrameTimingRecorder() + : null; + if (renderQueue.current != null) { + // If a scene is already queued up, drop it and queue this one up instead + // so that the scene view always displays the most recently requested scene. + renderQueue.next?.completer.complete(); + final Completer completer = Completer(); + renderQueue.next = (scene: scene, completer: completer, recorder: recorder); + return completer.future; + } + final Completer completer = Completer(); + renderQueue.current = (scene: scene, completer: completer, recorder: recorder); + unawaited(_kickRenderLoop(rasterizer)); + return completer.future; + } + + Future _kickRenderLoop(ViewRasterizer rasterizer) async { + final RenderQueue renderQueue = rasterizer.queue; + while (renderQueue.current != null) { + final RenderRequest current = renderQueue.current!; + try { + await _renderScene(current.scene, rasterizer, current.recorder); + current.completer.complete(); + } catch (error, stackTrace) { + current.completer.completeError(error, stackTrace); + } + renderQueue.current = renderQueue.next; + renderQueue.next = null; + } + } + + Future _renderScene( + ui.Scene scene, + ViewRasterizer rasterizer, + FrameTimingRecorder? recorder, + ) async { + await rasterizer.draw((scene as LayerScene).layerTree, recorder); + recorder?.submitTimings(); + } void dumpDebugInfo(); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart deleted file mode 100644 index 1031dc9c51c..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_builder.dart +++ /dev/null @@ -1,489 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -// This file implements a SceneBuilder and Scene that works with any renderer -// implementation that provides: -// * A `ui.Canvas` that conforms to `SceneCanvas` -// * A `ui.Picture` that conforms to `ScenePicture` -// * A `ui.ImageFilter` that conforms to `SceneImageFilter` -// -// These contain a few augmentations to the normal `dart:ui` API that provide -// additional sizing information that the scene builder uses to determine how -// these object might occlude one another. - -class EngineScene implements ui.Scene { - EngineScene(this.rootLayer); - - final EngineRootLayer rootLayer; - - // We keep a refcount here because this can be asynchronously rendered, so we - // don't necessarily want to dispose immediately when the user calls dispose. - // Instead, we need to stay alive until we're done rendering. - int _refCount = 1; - - void beginRender() { - assert(_refCount > 0); - _refCount++; - } - - void endRender() { - _refCount--; - _disposeIfNeeded(); - } - - @override - void dispose() { - _refCount--; - _disposeIfNeeded(); - } - - void _disposeIfNeeded() { - assert(_refCount >= 0); - if (_refCount == 0) { - rootLayer.dispose(); - } - } - - @override - Future toImage(int width, int height) async { - return toImageSync(width, height); - } - - @override - ui.Image toImageSync(int width, int height) { - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final ui.Rect canvasRect = ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()); - final ui.Canvas canvas = ui.Canvas(recorder, canvasRect); - - // Only rasterizes the pictures. - for (final LayerSlice? slice in rootLayer.slices) { - if (slice != null) { - canvas.drawPicture(slice.picture); - } - } - return recorder.endRecording().toImageSync(width, height); - } - - Map get debugJsonDescription { - return {'rootLayer': rootLayer.debugJsonDescription}; - } -} - -sealed class OcclusionMapNode { - bool overlaps(ui.Rect rect); - OcclusionMapNode insert(ui.Rect rect); - ui.Rect get boundingBox; -} - -class OcclusionMapEmpty implements OcclusionMapNode { - @override - ui.Rect get boundingBox => ui.Rect.zero; - - @override - OcclusionMapNode insert(ui.Rect rect) => OcclusionMapLeaf(rect); - - @override - bool overlaps(ui.Rect rect) => false; -} - -class OcclusionMapLeaf implements OcclusionMapNode { - OcclusionMapLeaf(this.rect); - - final ui.Rect rect; - - @override - ui.Rect get boundingBox => rect; - - @override - OcclusionMapNode insert(ui.Rect other) => OcclusionMapBranch(this, OcclusionMapLeaf(other)); - - @override - bool overlaps(ui.Rect other) => rect.overlaps(other); -} - -class OcclusionMapBranch implements OcclusionMapNode { - OcclusionMapBranch(this.left, this.right) - : boundingBox = left.boundingBox.expandToInclude(right.boundingBox); - - final OcclusionMapNode left; - final OcclusionMapNode right; - - @override - final ui.Rect boundingBox; - - double _areaOfUnion(ui.Rect first, ui.Rect second) { - return (math.max(first.right, second.right) - math.min(first.left, second.left)) * - (math.max(first.bottom, second.bottom) - math.max(first.top, second.top)); - } - - @override - OcclusionMapNode insert(ui.Rect other) { - // Try to create nodes with the smallest possible area - final double leftOtherArea = _areaOfUnion(left.boundingBox, other); - final double rightOtherArea = _areaOfUnion(right.boundingBox, other); - final double leftRightArea = boundingBox.width * boundingBox.height; - if (leftOtherArea < rightOtherArea) { - if (leftOtherArea < leftRightArea) { - return OcclusionMapBranch(left.insert(other), right); - } - } else { - if (rightOtherArea < leftRightArea) { - return OcclusionMapBranch(left, right.insert(other)); - } - } - return OcclusionMapBranch(this, OcclusionMapLeaf(other)); - } - - @override - bool overlaps(ui.Rect rect) { - if (!boundingBox.overlaps(rect)) { - return false; - } - return left.overlaps(rect) || right.overlaps(rect); - } -} - -class OcclusionMap { - OcclusionMapNode root = OcclusionMapEmpty(); - - void addRect(ui.Rect rect) => root = root.insert(rect); - - bool overlaps(ui.Rect rect) => root.overlaps(rect); -} - -class SceneSlice { - final OcclusionMap pictureOcclusionMap = OcclusionMap(); - final OcclusionMap platformViewOcclusionMap = OcclusionMap(); -} - -class EngineSceneBuilder implements ui.SceneBuilder { - LayerBuilder currentBuilder = LayerBuilder.rootLayer(); - - final List sceneSlices = [SceneSlice()]; - - // This represents the simplest case with no platform views, which is a fast path - // that allows us to avoid work tracking the pictures themselves. - bool _isSimple = true; - - @override - void addPerformanceOverlay(int enabledOptions, ui.Rect bounds) { - // We don't plan to implement this on the web. - throw UnimplementedError(); - } - - @override - void addPicture( - ui.Offset offset, - ui.Picture picture, { - bool isComplexHint = false, - bool willChangeHint = false, - }) { - final int sliceIndex = _placePicture( - offset, - picture as ScenePicture, - currentBuilder.globalPlatformViewStyling, - ); - currentBuilder.addPicture(offset, picture, sliceIndex: sliceIndex); - } - - // This function determines the lowest scene slice that this picture can be placed - // into and adds it to that slice's occlusion map. - // - // The picture is placed in the last slice where it either intersects with a picture - // in the slice or it intersects with a platform view in the preceding slice. If the - // picture intersects with a platform view in the last slice, a new slice is added at - // the end and the picture goes in there. - int _placePicture(ui.Offset offset, ScenePicture picture, PlatformViewStyling styling) { - if (_isSimple) { - // This is the fast path where there are no platform views. The picture should - // just be placed on the bottom (and only) slice. - return 0; - } - final ui.Rect cullRect = picture.cullRect.shift(offset); - final ui.Rect mappedCullRect = styling.mapLocalToGlobal(cullRect); - int sliceIndex = sceneSlices.length; - while (sliceIndex > 0) { - final SceneSlice sliceBelow = sceneSlices[sliceIndex - 1]; - if (sliceBelow.platformViewOcclusionMap.overlaps(mappedCullRect)) { - break; - } - sliceIndex--; - if (sliceBelow.pictureOcclusionMap.overlaps(mappedCullRect)) { - break; - } - } - if (sliceIndex == 0) { - // Don't bother to populate the lowest occlusion map with pictures, since - // we never hit test against pictures in the bottom slice. - return sliceIndex; - } - if (sliceIndex == sceneSlices.length) { - // Insert a new slice. - sceneSlices.add(SceneSlice()); - } - final SceneSlice slice = sceneSlices[sliceIndex]; - slice.pictureOcclusionMap.addRect(mappedCullRect); - return sliceIndex; - } - - @override - void addPlatformView( - int viewId, { - ui.Offset offset = ui.Offset.zero, - double width = 0.0, - double height = 0.0, - }) { - final ui.Rect platformViewRect = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); - final int sliceIndex = _placePlatformView( - viewId, - platformViewRect, - currentBuilder.globalPlatformViewStyling, - ); - currentBuilder.addPlatformView(viewId, bounds: platformViewRect, sliceIndex: sliceIndex); - } - - // This function determines the lowest scene slice this platform view can be placed - // into and adds it to that slice's occlusion map. - // - // The platform view is placed into the last slice where it intersects with a picture - // or a platform view. - int _placePlatformView(int viewId, ui.Rect rect, PlatformViewStyling styling) { - // Once we add a platform view, we actually have to do proper occlusion tracking. - _isSimple = false; - - final ui.Rect globalPlatformViewRect = styling.mapLocalToGlobal(rect); - int sliceIndex = sceneSlices.length - 1; - while (sliceIndex > 0) { - final SceneSlice slice = sceneSlices[sliceIndex]; - if (slice.platformViewOcclusionMap.overlaps(globalPlatformViewRect) || - slice.pictureOcclusionMap.overlaps(globalPlatformViewRect)) { - break; - } - sliceIndex--; - } - final SceneSlice slice = sceneSlices[sliceIndex]; - slice.platformViewOcclusionMap.addRect(globalPlatformViewRect); - return sliceIndex; - } - - @override - void addRetained(ui.EngineLayer retainedLayer) { - final PictureEngineLayer placedEngineLayer = _placeRetainedLayer( - retainedLayer as PictureEngineLayer, - currentBuilder.globalPlatformViewStyling, - ); - currentBuilder.mergeLayer(placedEngineLayer); - } - - PictureEngineLayer _placeRetainedLayer( - PictureEngineLayer retainedLayer, - PlatformViewStyling styling, - ) { - if (_isSimple && retainedLayer.isSimple) { - // There are no platform views, so we don't need to do any occlusion tracking - // and can simply merge the layer. - return retainedLayer; - } - bool needsRebuild = false; - final List revisedDrawCommands = []; - final PlatformViewStyling combinedStyling = PlatformViewStyling.combine( - styling, - retainedLayer.platformViewStyling, - ); - for (final LayerDrawCommand command in retainedLayer.drawCommands) { - switch (command) { - case PictureDrawCommand( - offset: final ui.Offset offset, - picture: final ScenePicture picture, - ): - final int sliceIndex = _placePicture(offset, picture, combinedStyling); - if (command.sliceIndex != sliceIndex) { - needsRebuild = true; - } - revisedDrawCommands.add(PictureDrawCommand(offset, picture, sliceIndex)); - case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): - final int sliceIndex = _placePlatformView(viewId, bounds, combinedStyling); - if (command.sliceIndex != sliceIndex) { - needsRebuild = true; - } - revisedDrawCommands.add(PlatformViewDrawCommand(viewId, bounds, sliceIndex)); - case RetainedLayerDrawCommand(layer: final PictureEngineLayer sublayer): - final PictureEngineLayer revisedSublayer = _placeRetainedLayer(sublayer, combinedStyling); - if (sublayer != revisedSublayer) { - needsRebuild = true; - } - revisedDrawCommands.add(RetainedLayerDrawCommand(revisedSublayer)); - } - } - - if (!needsRebuild) { - // No elements changed which slice position they are in, so we can simply - // merge the existing layer down and don't have to redraw individual elements. - return retainedLayer; - } - - // Otherwise, we replace the commands of the layer to create a new one. - currentBuilder = LayerBuilder.childLayer( - parent: currentBuilder, - layer: retainedLayer.emptyClone(), - ); - for (final LayerDrawCommand command in revisedDrawCommands) { - switch (command) { - case PictureDrawCommand( - offset: final ui.Offset offset, - picture: final ScenePicture picture, - ): - currentBuilder.addPicture(offset, picture, sliceIndex: command.sliceIndex); - case PlatformViewDrawCommand(viewId: final int viewId, bounds: final ui.Rect bounds): - currentBuilder.addPlatformView(viewId, bounds: bounds, sliceIndex: command.sliceIndex); - case RetainedLayerDrawCommand(layer: final PictureEngineLayer layer): - currentBuilder.mergeLayer(layer); - } - } - final PictureEngineLayer newLayer = currentBuilder.build(); - currentBuilder = currentBuilder.parent!; - return newLayer; - } - - @override - void addTexture( - int textureId, { - ui.Offset offset = ui.Offset.zero, - double width = 0.0, - double height = 0.0, - bool freeze = false, - ui.FilterQuality filterQuality = ui.FilterQuality.low, - }) { - // addTexture is not implemented on web. - } - - @override - ui.BackdropFilterEngineLayer pushBackdropFilter( - ui.ImageFilter filter, { - ui.BlendMode blendMode = ui.BlendMode.srcOver, - ui.BackdropFilterEngineLayer? oldLayer, - int? backdropId, - }) => pushLayer( - BackdropFilterLayer(BackdropFilterOperation(filter, blendMode)), - ); - - @override - ui.ClipPathEngineLayer pushClipPath( - ui.Path path, { - ui.Clip clipBehavior = ui.Clip.antiAlias, - ui.ClipPathEngineLayer? oldLayer, - }) => pushLayer(ClipPathLayer(ClipPathOperation(path as ScenePath, clipBehavior))); - - @override - ui.ClipRRectEngineLayer pushClipRRect( - ui.RRect rrect, { - required ui.Clip clipBehavior, - ui.ClipRRectEngineLayer? oldLayer, - }) => pushLayer(ClipRRectLayer(ClipRRectOperation(rrect, clipBehavior))); - - @override - ui.ClipRSuperellipseEngineLayer pushClipRSuperellipse( - ui.RSuperellipse rsuperellipse, { - required ui.Clip clipBehavior, - ui.ClipRSuperellipseEngineLayer? oldLayer, - }) { - return pushLayer( - ClipRSuperellipseLayer(ClipRSuperellipseOperation(rsuperellipse, clipBehavior)), - ); - } - - @override - ui.ClipRectEngineLayer pushClipRect( - ui.Rect rect, { - ui.Clip clipBehavior = ui.Clip.antiAlias, - ui.ClipRectEngineLayer? oldLayer, - }) { - return pushLayer(ClipRectLayer(ClipRectOperation(rect, clipBehavior))); - } - - @override - ui.ColorFilterEngineLayer pushColorFilter( - ui.ColorFilter filter, { - ui.ColorFilterEngineLayer? oldLayer, - }) => pushLayer(ColorFilterLayer(ColorFilterOperation(filter))); - - @override - ui.ImageFilterEngineLayer pushImageFilter( - ui.ImageFilter filter, { - ui.Offset offset = ui.Offset.zero, - ui.ImageFilterEngineLayer? oldLayer, - }) => pushLayer( - ImageFilterLayer(ImageFilterOperation(filter as SceneImageFilter, offset)), - ); - - @override - ui.OffsetEngineLayer pushOffset(double dx, double dy, {ui.OffsetEngineLayer? oldLayer}) => - pushLayer(OffsetLayer(OffsetOperation(dx, dy))); - - @override - ui.OpacityEngineLayer pushOpacity( - int alpha, { - ui.Offset offset = ui.Offset.zero, - ui.OpacityEngineLayer? oldLayer, - }) => pushLayer(OpacityLayer(OpacityOperation(alpha, offset))); - - @override - ui.ShaderMaskEngineLayer pushShaderMask( - ui.Shader shader, - ui.Rect maskRect, - ui.BlendMode blendMode, { - ui.ShaderMaskEngineLayer? oldLayer, - ui.FilterQuality filterQuality = ui.FilterQuality.low, - }) => - pushLayer(ShaderMaskLayer(ShaderMaskOperation(shader, maskRect, blendMode))); - - @override - ui.TransformEngineLayer pushTransform(Float64List matrix4, {ui.TransformEngineLayer? oldLayer}) => - pushLayer(TransformLayer(TransformOperation(matrix4))); - - @override - void setProperties( - double width, - double height, - double insetTop, - double insetRight, - double insetBottom, - double insetLeft, - bool focusable, - ) { - // Not implemented on web - } - - @override - ui.Scene build() { - while (currentBuilder.parent != null) { - pop(); - } - final PictureEngineLayer rootLayer = currentBuilder.build(); - return EngineScene(rootLayer as EngineRootLayer); - } - - @override - void pop() { - final LayerBuilder? parentBuilder = currentBuilder.parent; - if (parentBuilder == null) { - // Root layer. Nothing to pop. - return; - } - final PictureEngineLayer layer = currentBuilder.build(); - currentBuilder = parentBuilder; - currentBuilder.mergeLayer(layer); - } - - T pushLayer(T layer) { - currentBuilder = LayerBuilder.childLayer(parent: currentBuilder, layer: layer); - return layer; - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart deleted file mode 100644 index ae6cc8c8692..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_painting.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:ui/ui.dart' as ui; - -import 'vector_math.dart'; - -// These are additional APIs that are not part of the `dart:ui` interface that -// are needed internally to properly implement a `SceneBuilder` on top of the -// generic Canvas/Picture api. -abstract class SceneCanvas implements ui.Canvas { - // This is the same as a normal `saveLayer` call, but we can pass a backdrop image filter. - void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter backdropFilter); -} - -abstract class ScenePicture implements ui.Picture { - // This is a conservative bounding box of all the drawing primitives in this picture. - ui.Rect get cullRect; -} - -abstract class ScenePictureRecorder implements ui.PictureRecorder { - /// Whether this reference to the underlying picture recorder is [dispose]d. - /// - /// This only returns a valid value if asserts are enabled, and must not be - /// used otherwise. - bool get debugDisposed; -} - -abstract class SceneImageFilter implements ui.ImageFilter { - // Since some image filters affect the actual drawing bounds of a given picture, this - // gives the maximum draw boundary for a picture with the given input bounds after it - // has been processed by the filter. - ui.Rect filterBounds(ui.Rect inputBounds); - - // The matrix image filter changes the position of the content, so when positioning - // platform views and calculating occlusion we need to take its transform into account. - Matrix4? get transform; -} - -abstract class ScenePath implements ui.Path { - // In order to properly clip platform views with paths, we need to be able to get a - // string representation of them. - String toSvgString(); -} 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 deleted file mode 100644 index 6a3cd089065..00000000000 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/scene_view.dart +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -const String kCanvasContainerTag = 'flt-canvas-container'; - -typedef RenderResult = ({ - List imageBitmaps, - int rasterStartMicros, - int rasterEndMicros, -}); - -// This is an interface that renders a `ScenePicture` as a `DomImageBitmap`. -// It is optionally asynchronous. It is required for the `EngineSceneView` to -// composite pictures into the canvases in the DOM tree it builds. -abstract class PictureRenderer { - FutureOr renderPictures(List picture, int width, int height); -} - -class _SceneRender { - _SceneRender(this.scene, this._completer, {this.recorder}) { - scene.beginRender(); - } - - final EngineScene scene; - final Completer _completer; - final FrameTimingRecorder? recorder; - - void done() { - scene.endRender(); - _completer.complete(); - } -} - -// This class builds a DOM tree that composites an `EngineScene`. -class EngineSceneView { - factory EngineSceneView(PictureRenderer pictureRenderer, EngineFlutterView flutterView) { - final DomElement sceneElement = createDomElement('flt-scene'); - return EngineSceneView._(pictureRenderer, flutterView, sceneElement); - } - - EngineSceneView._(this.pictureRenderer, this.flutterView, this.sceneElement); - - final PictureRenderer pictureRenderer; - final DomElement sceneElement; - final EngineFlutterView flutterView; - - List containers = []; - - _SceneRender? _currentRender; - _SceneRender? _nextRender; - - // Only populated in debug mode. - _SceneRender? _previousRender; - - Future renderScene(EngineScene scene, FrameTimingRecorder? recorder) { - if (_currentRender != null) { - // If a scene is already queued up, drop it and queue this one up instead - // so that the scene view always displays the most recently requested scene. - _nextRender?.done(); - final Completer completer = Completer(); - _nextRender = _SceneRender(scene, completer, recorder: recorder); - return completer.future; - } - final Completer completer = Completer(); - _currentRender = _SceneRender(scene, completer, recorder: recorder); - _kickRenderLoop(); - return completer.future; - } - - Future _kickRenderLoop() async { - final _SceneRender current = _currentRender!; - await _renderScene(current.scene, current.recorder); - _renderComplete(current); - _currentRender = _nextRender; - _nextRender = null; - if (_currentRender == null) { - return; - } else { - return _kickRenderLoop(); - } - } - - 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 = []; - for (final LayerSlice? slice in slices) { - if (slice == null) { - continue; - } - 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 { - picturesToRender.add(slice.picture); - } - } - final Map renderMap; - if (picturesToRender.isNotEmpty) { - final RenderResult renderResult = await pictureRenderer.renderPictures( - picturesToRender, - screenBounds.width.ceil(), - screenBounds.height.ceil(), - ); - renderMap = { - for (int i = 0; i < picturesToRender.length; i++) - picturesToRender[i]: renderResult.imageBitmaps[i], - }; - recorder?.recordRasterStart(renderResult.rasterStartMicros); - recorder?.recordRasterFinish(renderResult.rasterEndMicros); - } else { - renderMap = {}; - recorder?.recordRasterStart(); - recorder?.recordRasterFinish(); - } - recorder?.submitTimings(); - - final List reusableContainers = List.from(containers); - final List newContainers = []; - for (final LayerSlice? slice in slices) { - if (slice == null) { - continue; - } - final DomImageBitmap? bitmap = renderMap[slice.picture]; - if (bitmap != null) { - PictureSliceContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PictureSliceContainer) { - container = candidate; - reusableContainers[j] = null; - break; - } - } - - if (container != null) { - container.bounds = screenBounds; - } else { - container = PictureSliceContainer(screenBounds); - } - container.updateContents(); - container.renderBitmap(bitmap); - newContainers.add(container); - } - - for (final PlatformView view in slice.platformViews) { - // Ensure the contents of the platform view are injected into the DOM. - flutterView.dom.injectPlatformView(view.viewId); - - // Attempt to reuse a container for the existing view - PlatformViewContainer? container; - for (int j = 0; j < reusableContainers.length; j++) { - final SliceContainer? candidate = reusableContainers[j]; - if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { - container = candidate; - reusableContainers[j] = null; - break; - } - } - container ??= PlatformViewContainer(view.viewId); - container.bounds = view.bounds; - container.styling = view.styling; - container.updateContents(); - newContainers.add(container); - } - } - - for (final SliceContainer? container in reusableContainers) { - if (container != null) { - sceneElement.removeChild(container.container); - } - } - - containers = newContainers; - - DomElement? currentElement = sceneElement.firstElementChild; - for (final SliceContainer container in containers) { - if (currentElement == null) { - sceneElement.appendChild(container.container); - } else if (currentElement == container.container) { - currentElement = currentElement.nextElementSibling; - } else { - sceneElement.insertBefore(container.container, currentElement); - } - } - } - - void _renderComplete(_SceneRender render) { - render.done(); - if (kDebugMode) { - _previousRender = render; - } - } - - Map? dumpDebugInfo() { - if (kDebugMode && _previousRender != null) { - return _previousRender!.scene.debugJsonDescription; - } - return null; - } -} - -sealed class SliceContainer { - DomElement get container; - - void updateContents(); -} - -final class PictureSliceContainer extends SliceContainer { - factory PictureSliceContainer(ui.Rect bounds) { - final DomElement container = domDocument.createElement(kCanvasContainerTag); - final DomHTMLCanvasElement canvas = createDomCanvasElement( - width: bounds.width.toInt(), - height: bounds.height.toInt(), - ); - container.appendChild(canvas); - return PictureSliceContainer._(bounds, container, canvas); - } - - PictureSliceContainer._(this._bounds, this.container, this.canvas); - - ui.Rect _bounds; - bool _dirty = true; - - ui.Rect get bounds => _bounds; - set bounds(ui.Rect bounds) { - if (_bounds != bounds) { - _bounds = bounds; - _dirty = true; - } - } - - @override - void updateContents() { - if (_dirty) { - _dirty = false; - - final ui.Rect roundedOutBounds = ui.Rect.fromLTRB( - bounds.left.floorToDouble(), - bounds.top.floorToDouble(), - bounds.right.ceilToDouble(), - bounds.bottom.ceilToDouble(), - ); - final DomCSSStyleDeclaration style = canvas.style; - final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio; - final double logicalWidth = roundedOutBounds.width / devicePixelRatio; - final double logicalHeight = roundedOutBounds.height / devicePixelRatio; - final double logicalLeft = roundedOutBounds.left / devicePixelRatio; - final double logicalTop = roundedOutBounds.top / devicePixelRatio; - style.width = '${logicalWidth}px'; - style.height = '${logicalHeight}px'; - style.position = 'absolute'; - style.left = '${logicalLeft}px'; - style.top = '${logicalTop}px'; - canvas.width = roundedOutBounds.width.ceilToDouble(); - canvas.height = roundedOutBounds.height.ceilToDouble(); - } - } - - void renderBitmap(DomImageBitmap bitmap) { - final DomImageBitmapRenderingContext ctx = canvas.contextBitmapRenderer; - ctx.transferFromImageBitmap(bitmap); - } - - @override - final DomElement container; - final DomHTMLCanvasElement canvas; -} - -final class PlatformViewContainer extends SliceContainer { - PlatformViewContainer(this.viewId) - : container = createDomElement('flt-clip'), - slot = createPlatformViewSlot(viewId) { - container.appendChild(slot); - } - - final int viewId; - PlatformViewStyling? _styling; - ui.Rect? _bounds; - bool _dirty = false; - - ui.Path? _clipPath; - String? _clipPathString; - - @override - final DomElement container; - - final DomElement slot; - - set styling(PlatformViewStyling styling) { - if (_styling != styling) { - _styling = styling; - _dirty = true; - } - } - - set bounds(ui.Rect bounds) { - if (_bounds != bounds) { - _bounds = bounds; - _dirty = true; - } - } - - set clipPath(ScenePath? path) { - if (_clipPath == path) { - return; - } - - _clipPath = path; - _clipPathString = path?.toSvgString(); - } - - String cssStringForClip(PlatformViewClip clip, double devicePixelRatio) { - switch (clip) { - case PlatformViewNoClip(): - clipPath = null; - return ''; - case PlatformViewRectClip(): - clipPath = null; - final double top = clip.rect.top / devicePixelRatio; - final double right = clip.rect.right / devicePixelRatio; - final double bottom = clip.rect.bottom / devicePixelRatio; - final double left = clip.rect.left / devicePixelRatio; - return 'rect(${top}px ${right}px ${bottom}px ${left}px)'; - case PlatformViewRRectClip(): - clipPath = null; - final double top = clip.rrect.top / devicePixelRatio; - final double right = clip.rrect.right / devicePixelRatio; - final double bottom = clip.rrect.bottom / devicePixelRatio; - final double left = clip.rrect.left / devicePixelRatio; - final double tlRadiusX = clip.rrect.tlRadiusX / devicePixelRatio; - final double tlRadiusY = clip.rrect.tlRadiusY / devicePixelRatio; - final double trRadiusX = clip.rrect.trRadiusX / devicePixelRatio; - final double trRadiusY = clip.rrect.trRadiusY / devicePixelRatio; - final double brRadiusX = clip.rrect.brRadiusX / devicePixelRatio; - final double brRadiusY = clip.rrect.brRadiusY / devicePixelRatio; - final double blRadiusX = clip.rrect.blRadiusX / devicePixelRatio; - final double blRadiusY = clip.rrect.blRadiusY / devicePixelRatio; - return 'rect(${top}px ${right}px ${bottom}px ${left}px round ${tlRadiusX}px ${trRadiusX}px ${brRadiusX}px ${blRadiusX}px / ${tlRadiusY}px ${trRadiusY}px ${brRadiusY}px ${blRadiusY}px)'; - case PlatformViewPathClip(): - ScenePath path = clip.path; - if (devicePixelRatio != 1.0) { - final dprTransform = Matrix4.identity()..scale(1 / devicePixelRatio); - path = path.transform(dprTransform.toFloat64()) as ScenePath; - } - clipPath = path; - return "path('$_clipPathString')"; - } - } - - @override - void updateContents() { - assert(_styling != null); - assert(_bounds != null); - if (_dirty) { - final DomCSSStyleDeclaration style = slot.style; - style.position = 'absolute'; - style.width = '${_bounds!.width}px'; - style.height = '${_bounds!.height}px'; - - final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio; - final PlatformViewPosition position = _styling!.position; - - final Matrix4 transform; - if (position.transform != null) { - transform = position.transform!.clone()..translate(_bounds!.left, _bounds!.top); - } else { - final ui.Offset offset = position.offset ?? ui.Offset.zero; - transform = Matrix4.translationValues( - _bounds!.left + offset.dx, - _bounds!.top + offset.dy, - 0, - ); - } - final double inverseScale = 1.0 / devicePixelRatio; - final Matrix4 scaleMatrix = Matrix4.diagonal3Values(inverseScale, inverseScale, 1); - scaleMatrix.multiply(transform); - style.transform = float64ListToCssTransform(scaleMatrix.storage); - style.transformOrigin = '0 0 0'; - style.opacity = _styling!.opacity != 1.0 ? '${_styling!.opacity}' : ''; - - final DomCSSStyleDeclaration containerStyle = container.style; - containerStyle.position = 'absolute'; - containerStyle.width = '100%'; - containerStyle.height = '100%'; - - final String clipPathString = cssStringForClip(_styling!.clip, devicePixelRatio); - containerStyle.clipPath = clipPathString; - - _dirty = false; - } - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart index b3667e9eb3d..2587ee04216 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart @@ -14,6 +14,7 @@ export 'skwasm_impl/filters.dart'; export 'skwasm_impl/font_collection.dart'; export 'skwasm_impl/image.dart'; export 'skwasm_impl/memory.dart'; +export 'skwasm_impl/offscreen_canvas_rasterizer.dart'; export 'skwasm_impl/paint.dart'; export 'skwasm_impl/paragraph.dart'; export 'skwasm_impl/path.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index 88b48cfcf55..2a2fc5a025d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -9,7 +9,7 @@ import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -class SkwasmCanvas implements SceneCanvas { +class SkwasmCanvas implements LayerCanvas { factory SkwasmCanvas(SkwasmPictureRecorder recorder, ui.Rect cullRect) => SkwasmCanvas.fromHandle( withStackScope( (StackScope s) => @@ -412,4 +412,16 @@ class SkwasmCanvas implements SceneCanvas { return scope.convertMatrix44FromNative(outMatrix); }); } + + @override + void clear(ui.Color color) { + canvasClear(_handle, color.value); + } + + @override + bool quickReject(ui.Rect rect) { + return withStackScope((StackScope s) { + return canvasQuickReject(_handle, s.convertRectToNative(rect)); + }); + } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart index c89d05c76d2..92766c29c17 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart @@ -11,7 +11,7 @@ import 'package:ui/ui.dart' as ui; typedef ImageFilterHandleBorrow = T Function(ImageFilterHandle handle); -abstract class SkwasmImageFilter implements SceneImageFilter { +abstract class SkwasmImageFilter implements LayerImageFilter { const SkwasmImageFilter(); factory SkwasmImageFilter.blur({ diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/offscreen_canvas_rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/offscreen_canvas_rasterizer.dart new file mode 100644 index 00000000000..6c9ad205d3e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/offscreen_canvas_rasterizer.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import '../skwasm_impl.dart'; + +/// A [Rasterizer] that uses a single GL context in an OffscreenCanvas to do +/// all the rendering. It transfers bitmaps created in the OffscreenCanvas to +/// one or many on-screen elements to actually display the scene. +class SkwasmOffscreenCanvasRasterizer extends Rasterizer { + SkwasmOffscreenCanvasRasterizer(this.offscreenSurface); + + /// This is an SkSurface backed by an OffScreenCanvas. This single Surface is + /// used to render to many RenderCanvases to produce the rendered scene. + final SkwasmSurface offscreenSurface; + + @override + SkwasmOffscreenCanvasViewRasterizer createViewRasterizer(EngineFlutterView view) { + return _viewRasterizers.putIfAbsent( + view, + () => SkwasmOffscreenCanvasViewRasterizer(view, this), + ); + } + + final Map _viewRasterizers = + {}; + + @override + void setResourceCacheMaxBytes(int bytes) { + offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + } + + @override + void dispose() { + offscreenSurface.dispose(); + for (final SkwasmOffscreenCanvasViewRasterizer viewRasterizer in _viewRasterizers.values) { + viewRasterizer.dispose(); + } + } +} + +class SkwasmOffscreenCanvasViewRasterizer extends ViewRasterizer { + SkwasmOffscreenCanvasViewRasterizer(super.view, this.rasterizer); + + final SkwasmOffscreenCanvasRasterizer rasterizer; + + @override + final DisplayCanvasFactory displayFactory = DisplayCanvasFactory( + createCanvas: () => RenderCanvas(), + ); + + @override + void prepareToDraw() { + // No need to do anything here. Skwasm sizes the surface in the `rasterize` + // call below. + } + + @override + Future rasterize( + List displayCanvases, + List pictures, + FrameTimingRecorder? recorder, + ) async { + if (displayCanvases.length != pictures.length) { + throw ArgumentError('Called rasterize() with a different number of canvases and pictures.'); + } + final RenderResult renderResult = await rasterizer.offscreenSurface.renderPictures( + pictures.cast(), + currentFrameSize.width, + currentFrameSize.height, + ); + recorder?.recordRasterStart(renderResult.rasterStartMicros); + recorder?.recordRasterFinish(renderResult.rasterEndMicros); + for (int i = 0; i < displayCanvases.length; i++) { + final RenderCanvas renderCanvas = displayCanvases[i] as RenderCanvas; + final DomImageBitmap bitmap = renderResult.imageBitmaps[i]; + renderCanvas.render(bitmap); + } + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart index 0486f450ed8..2e0a1050f50 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart @@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise } enum PathArcSize { small, large } -class SkwasmPath extends SkwasmObjectWrapper implements ScenePath, DisposablePath { +class SkwasmPath extends SkwasmObjectWrapper implements LayerPath, DisposablePath { factory SkwasmPath() { return SkwasmPath.fromHandle(pathCreate()); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart index 76b18a25be7..fd0cf72cb09 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/picture.dart @@ -6,7 +6,7 @@ import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -class SkwasmPicture extends SkwasmObjectWrapper implements ScenePicture { +class SkwasmPicture extends SkwasmObjectWrapper implements LayerPicture { SkwasmPicture.fromHandle(PictureHandle handle) : super(handle, _registry); static final SkwasmFinalizationRegistry _registry = @@ -39,7 +39,7 @@ class SkwasmPicture extends SkwasmObjectWrapper implements ScenePict } class SkwasmPictureRecorder extends SkwasmObjectWrapper - implements ScenePictureRecorder { + implements LayerPictureRecorder { SkwasmPictureRecorder() : super(pictureRecorderCreate(), _registry); static final SkwasmFinalizationRegistry _registry = diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart index 9dd5e49592d..f3026681621 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_canvas.dart @@ -63,6 +63,9 @@ external void canvasClipPath(CanvasHandle canvas, PathHandle path, bool antialia @Native(symbol: 'canvas_drawColor', isLeaf: true) external void canvasDrawColor(CanvasHandle canvas, int color, int blendMode); +@Native(symbol: 'canvas_clear', isLeaf: true) +external void canvasClear(CanvasHandle canvas, int color); + @Native( symbol: 'canvas_drawLine', isLeaf: true, @@ -255,3 +258,6 @@ external void canvasGetLocalClipBounds(CanvasHandle canvas, RawRect outRect); @Native(symbol: 'canvas_getDeviceClipBounds', isLeaf: true) external void canvasGetDeviceClipBounds(CanvasHandle canvas, RawIRect outRect); + +@Native(symbol: 'canvas_quickReject', isLeaf: true) +external bool canvasQuickReject(CanvasHandle canvas, RawRect rect); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart index 07ff8407c32..f5f60964eaf 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart @@ -33,6 +33,12 @@ external void surfaceSetCallbackHandler(SurfaceHandle surface, OnRenderCallbackH @Native(symbol: 'surface_destroy', isLeaf: true) external void surfaceDestroy(SurfaceHandle surface); +@Native( + symbol: 'surface_setResourceCacheLimitBytes', + isLeaf: true, +) +external void surfaceSetResourceCacheLimitBytes(SurfaceHandle surface, int bytes); + @Native, Int, Int, Int)>( symbol: 'surface_renderPictures', isLeaf: true, 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 930691002a0..2b68e2893d7 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 @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:ffi'; import 'dart:js_interop'; import 'dart:math' as math; @@ -16,8 +15,6 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web; class SkwasmRenderer extends Renderer { late SkwasmSurface surface; - final Map _sceneViews = - {}; bool get isMultiThreaded => skwasmIsMultiThreaded(); @@ -178,7 +175,7 @@ class SkwasmRenderer extends Renderer { ); @override - ui.SceneBuilder createSceneBuilder() => EngineSceneBuilder(); + ui.SceneBuilder createSceneBuilder() => LayerSceneBuilder(); @override ui.StrutStyle createStrutStyle({ @@ -324,7 +321,7 @@ class SkwasmRenderer extends Renderer { @override FutureOr initialize() { surface = SkwasmSurface(); - rasterizer = NoopRasterizer(); + rasterizer = SkwasmOffscreenCanvasRasterizer(surface); return super.initialize(); } @@ -399,25 +396,6 @@ class SkwasmRenderer extends Renderer { } } - @override - Future renderScene(ui.Scene scene, EngineFlutterView view) { - final FrameTimingRecorder? recorder = FrameTimingRecorder.frameTimingsEnabled - ? FrameTimingRecorder() - : null; - recorder?.recordBuildFinish(); - - final EngineSceneView sceneView = _getSceneViewForView(view); - return sceneView.renderScene(scene as EngineScene, recorder); - } - - EngineSceneView _getSceneViewForView(EngineFlutterView view) { - return _sceneViews.putIfAbsent(view, () { - final EngineSceneView sceneView = EngineSceneView(SkwasmPictureRenderer(surface), view); - view.dom.setScene(sceneView.sceneElement); - return sceneView; - }); - } - @override String get rendererTag => 'skwasm'; @@ -494,27 +472,6 @@ class SkwasmRenderer extends Renderer { ); } - String _generateDebugFilename(String filePrefix) { - final now = DateTime.now(); - final String y = now.year.toString().padLeft(4, '0'); - final String mo = now.month.toString().padLeft(2, '0'); - final String d = now.day.toString().padLeft(2, '0'); - final String h = now.hour.toString().padLeft(2, '0'); - final String mi = now.minute.toString().padLeft(2, '0'); - final String s = now.second.toString().padLeft(2, '0'); - return '$filePrefix-$y-$mo-$d-$h-$mi-$s.json'; - } - - void _dumpDebugInfo(String filePrefix, Map json) { - final String jsonString = const JsonEncoder.withIndent(' ').convert(json); - final blob = createDomBlob([jsonString], {'type': 'application/json'}); - final url = domWindow.URL.createObjectURL(blob); - final element = domDocument.createElement('a'); - element.setAttribute('href', url); - element.setAttribute('download', _generateDebugFilename(filePrefix)); - element.click(); - } - @override void dumpDebugInfo() { if (kDebugMode) { @@ -551,14 +508,14 @@ class SkwasmRenderer extends Renderer { 'surfaceCount': counts[26], 'verticesCount': counts[27], }; - _dumpDebugInfo('live_object_counts', countsJson); + downloadDebugInfo('live_object_counts', countsJson); }); int i = 0; - for (final view in _sceneViews.values) { - final Map? debugJson = view.dumpDebugInfo(); + for (final viewRasterizer in rasterizers.values) { + final Map? debugJson = viewRasterizer.dumpDebugInfo(); if (debugJson != null) { - _dumpDebugInfo('flutter-scene$i', debugJson); + downloadDebugInfo('flutter-scene$i', debugJson); i++; } } @@ -567,106 +524,6 @@ class SkwasmRenderer extends Renderer { @override void debugResetRasterizer() { - rasterizer = NoopRasterizer(); + rasterizer = SkwasmOffscreenCanvasRasterizer(surface); } } - -// A [Rasterizer] that does nothing. A placeholder that will be fleshed out -// when Skwasm and CanvasKit renderers are unified. See: https://github.com/flutter/flutter/issues/172311 -class NoopRasterizer implements Rasterizer { - @override - ViewRasterizer createViewRasterizer(EngineFlutterView view) { - return NoopViewRasterizer(); - } - - @override - void dispose() { - // Do nothing - } - - @override - void setResourceCacheMaxBytes(int bytes) { - // Do nothing - } -} - -// A [ViewRasterizer] that does nothing. A placeholder that will be fleshed out -// when Skwasm and CanvasKit renderers are unified. See: https://github.com/flutter/flutter/issues/172311 -class NoopViewRasterizer implements ViewRasterizer { - @override - BitmapSize currentFrameSize = BitmapSize.zero; - - @override - CompositorContext get context => throw UnimplementedError(); - - @override - void debugClear() { - // Do nothing - } - - @override - DisplayCanvasFactory get displayFactory => throw UnimplementedError(); - - @override - void dispose() { - // Do nothing - } - - @override - DisplayCanvas getOverlay() { - throw UnimplementedError(); - } - - @override - void prepareToDraw() {} - - @override - RenderQueue get queue => throw UnimplementedError(); - - @override - void releaseOverlay(DisplayCanvas overlay) {} - - @override - void releaseOverlays() {} - - @override - void removeOverlaysFromDom() {} - - @override - DomElement get sceneElement => throw UnimplementedError(); - - @override - EngineFlutterView get view => throw UnimplementedError(); - - @override - PlatformViewEmbedder get viewEmbedder => throw UnimplementedError(); - - @override - Map? dumpDebugInfo() { - throw UnimplementedError(); - } - - @override - Future rasterize( - List displayCanvases, - List pictures, - FrameTimingRecorder? recorder, - ) { - throw UnimplementedError(); - } - - @override - Future draw(LayerTree layerTree, FrameTimingRecorder? recorder) { - throw UnimplementedError(); - } -} - -class SkwasmPictureRenderer implements PictureRenderer { - SkwasmPictureRenderer(this.surface); - - SkwasmSurface surface; - - @override - FutureOr renderPictures(List pictures, int width, int height) => - surface.renderPictures(pictures.cast(), width, height); -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart index c109d1dfccc..2a185763758 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart @@ -68,6 +68,12 @@ class SkwasmCallbackHandler { } } +typedef RenderResult = ({ + List imageBitmaps, + int rasterStartMicros, + int rasterEndMicros, +}); + class SkwasmSurface { factory SkwasmSurface() { final SurfaceHandle surfaceHandle = withStackScope((StackScope scope) { @@ -127,6 +133,10 @@ class SkwasmSurface { return ByteData.sublistView(output); } + void setSkiaResourceCacheMaxBytes(int bytes) { + surfaceSetResourceCacheLimitBytes(handle, bytes); + } + void dispose() { surfaceDestroy(handle); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart index ad68bcd1d2e..bf417f9ecb8 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:convert'; import 'dart:math' as math; import 'dart:typed_data'; @@ -798,3 +799,24 @@ class BitmapSize { static const BitmapSize zero = BitmapSize(0, 0); } + +String _generateDebugFilename(String filePrefix) { + final now = DateTime.now(); + final String y = now.year.toString().padLeft(4, '0'); + final String mo = now.month.toString().padLeft(2, '0'); + final String d = now.day.toString().padLeft(2, '0'); + final String h = now.hour.toString().padLeft(2, '0'); + final String mi = now.minute.toString().padLeft(2, '0'); + final String s = now.second.toString().padLeft(2, '0'); + return '$filePrefix-$y-$mo-$d-$h-$mi-$s.json'; +} + +void downloadDebugInfo(String filePrefix, Map json) { + final String jsonString = const JsonEncoder.withIndent(' ').convert(json); + final blob = createDomBlob([jsonString], {'type': 'application/json'}); + final url = domWindow.URL.createObjectURL(blob); + final element = domDocument.createElement('a'); + element.setAttribute('href', url); + element.setAttribute('download', _generateDebugFilename(filePrefix)); + element.click(); +} diff --git a/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp b/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp index e6653eddbff..973257f6722 100644 --- a/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp +++ b/engine/src/flutter/lib/web_ui/skwasm/canvas.cpp @@ -172,6 +172,10 @@ SKWASM_EXPORT void canvas_transform(DisplayListBuilder* canvas, canvas->Transform(*matrix44); } +SKWASM_EXPORT void canvas_clear(DisplayListBuilder* canvas, uint32_t color) { + canvas->DrawColor(DlColor(color), DlBlendMode::kSrc); +} + SKWASM_EXPORT void canvas_clipRect(DisplayListBuilder* canvas, const DlRect* rect, DlClipOp op, @@ -365,3 +369,8 @@ SKWASM_EXPORT void canvas_getDeviceClipBounds(DisplayListBuilder* canvas, DlIRect* outRect) { *outRect = DlIRect::RoundOut(canvas->GetDestinationClipCoverage()); } + +SKWASM_EXPORT bool canvas_quickReject(DisplayListBuilder* canvas, + DlRect* rect) { + return canvas->QuickReject(*rect); +} diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface.cpp b/engine/src/flutter/lib/web_ui/skwasm/surface.cpp index f430af73a01..5ee63d54d46 100644 --- a/engine/src/flutter/lib/web_ui/skwasm/surface.cpp +++ b/engine/src/flutter/lib/web_ui/skwasm/surface.cpp @@ -42,6 +42,11 @@ void Surface::dispose() { delete this; } +// Main thread only +void Surface::setResourceCacheLimit(int bytes) { + _grContext->setResourceCacheLimit(bytes); +} + // Main thread only uint32_t Surface::renderPictures(DisplayList** pictures, int width, @@ -268,6 +273,11 @@ SKWASM_EXPORT void surface_dispose(Surface* surface) { surface->dispose(); } +SKWASM_EXPORT void surface_setResourceCacheLimitBytes(Surface* surface, + int bytes) { + surface->setResourceCacheLimit(bytes); +} + SKWASM_EXPORT uint32_t surface_renderPictures(Surface* surface, DisplayList** pictures, int width, diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface.h b/engine/src/flutter/lib/web_ui/skwasm/surface.h index 867a09e6801..909c32c1564 100644 --- a/engine/src/flutter/lib/web_ui/skwasm/surface.h +++ b/engine/src/flutter/lib/web_ui/skwasm/surface.h @@ -58,6 +58,7 @@ class Surface { // Main thread only void dispose(); + void setResourceCacheLimit(int bytes); uint32_t renderPictures(flutter::DisplayList** picture, int width, int height, diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart deleted file mode 100644 index 27c4f7abac5..00000000000 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_test.dart +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; - -import 'package:ui/ui.dart' as ui; - -import 'scene_builder_utils.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - setUpAll(() { - LayerSliceBuilder.debugRecorderFactory = (ui.Rect rect) { - final StubSceneCanvas canvas = StubSceneCanvas(); - final StubPictureRecorder recorder = StubPictureRecorder(canvas); - return (recorder, canvas); - }; - }); - - tearDownAll(() { - LayerSliceBuilder.debugRecorderFactory = null; - }); - - group('EngineSceneBuilder', () { - test('single picture', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 1); - expect(slices[0], layerSlice(withPictureRect: pictureRect)); - }); - - test('two pictures', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 300, 400, 400); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 1); - expect(slices[0], layerSlice(withPictureRect: const ui.Rect.fromLTRB(100, 100, 400, 400))); - }); - - test('picture + platform view (overlapping)', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); - sceneBuilder.addPlatformView( - 1, - offset: platformViewRect.topLeft, - width: platformViewRect.width, - height: platformViewRect.height, - ); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 1); - expect( - slices[0], - layerSlice( - withPictureRect: pictureRect, - withPlatformViews: [ - PlatformView(1, platformViewRect, const PlatformViewStyling()), - ], - ), - ); - }); - - test('platform view + picture (overlapping)', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); - sceneBuilder.addPlatformView( - 1, - offset: platformViewRect.topLeft, - width: platformViewRect.width, - height: platformViewRect.height, - ); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect( - slices[0], - layerSlice( - withPlatformViews: [ - PlatformView(1, platformViewRect, const PlatformViewStyling()), - ], - ), - ); - expect(slices[1], layerSlice(withPictureRect: pictureRect)); - }); - - test('platform view sandwich (overlapping)', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); - const ui.Rect pictureRect2 = ui.Rect.fromLTRB(200, 200, 300, 300); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); - sceneBuilder.addPlatformView( - 1, - offset: platformViewRect.topLeft, - width: platformViewRect.width, - height: platformViewRect.height, - ); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect( - slices[0], - layerSlice( - withPictureRect: pictureRect1, - withPlatformViews: [ - PlatformView(1, platformViewRect, const PlatformViewStyling()), - ], - ), - ); - expect(slices[1], layerSlice(withPictureRect: pictureRect2)); - }); - - test('platform view sandwich (non-overlapping)', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); - const ui.Rect pictureRect2 = ui.Rect.fromLTRB(50, 50, 100, 100); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); - sceneBuilder.addPlatformView( - 1, - offset: platformViewRect.topLeft, - width: platformViewRect.width, - height: platformViewRect.height, - ); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - - // The top picture does not overlap with the platform view, so it should - // be grouped into the slice below it to reduce the number of canvases we - // need. - expect(slices.length, 1); - expect( - slices[0], - layerSlice( - withPictureRect: const ui.Rect.fromLTRB(50, 50, 200, 200), - withPlatformViews: [ - PlatformView(1, platformViewRect, const PlatformViewStyling()), - ], - ), - ); - }); - - test('platform view sandwich (overlapping) with offset layers', () { - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); - - sceneBuilder.pushOffset(150, 150); - const ui.Rect platformViewRect = ui.Rect.fromLTRB(0, 0, 100, 100); - sceneBuilder.addPlatformView( - 1, - offset: platformViewRect.topLeft, - width: platformViewRect.width, - height: platformViewRect.height, - ); - sceneBuilder.pushOffset(50, 50); - sceneBuilder.addPicture(ui.Offset.zero, StubPicture(const ui.Rect.fromLTRB(0, 0, 100, 100))); - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect( - slices[0], - layerSlice( - withPictureRect: pictureRect1, - withPlatformViews: [ - PlatformView( - 1, - platformViewRect, - const PlatformViewStyling(position: PlatformViewPosition.offset(ui.Offset(150, 150))), - ), - ], - ), - ); - expect(slices[1], layerSlice(withPictureRect: const ui.Rect.fromLTRB(200, 200, 300, 300))); - }); - - test('nested offset and clip', () { - final EngineSceneBuilder builder = EngineSceneBuilder(); - { - builder.pushOffset(16, 43); - { - builder.pushClipRect( - const ui.Rect.fromLTRB(0, 0, 1007, 1122), - clipBehavior: ui.Clip.hardEdge, - ); - { - builder.pushOffset(1, 214); - builder.addPlatformView(1, width: 1005, height: 907); - builder.pop(); - } - builder.pop(); - } - builder.pop(); - } - - const ui.Rect pictureRect = ui.Rect.fromLTRB(16, 198, 310, 280); - builder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); - - final EngineScene scene = builder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - expect(slices.length, 2); - expect( - slices[0], - layerSlice( - withPlatformViews: [ - PlatformView( - 1, - const ui.Rect.fromLTRB(0, 0, 1005, 907), - const PlatformViewStyling( - position: PlatformViewPosition.offset(ui.Offset(17, 257)), - clip: PlatformViewRectClip(ui.Rect.fromLTRB(16.0, 43.0, 1023.0, 1165.0)), - ), - ), - ], - ), - ); - expect(slices[1], layerSlice(withPictureRect: pictureRect)); - }); - - test('grid view test', () { - // This test case covers a grid of elements, where each element is a platform - // view that has flutter content underneath it and on top of it. - // See a detailed explanation of this use-case in the following flutter issue: - // https://github.com/flutter/flutter/issues/149863 - final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); - - const double padding = 10; - const double tileSize = 50; - final List expectedPlatformViews = []; - for (int x = 0; x < 10; x++) { - for (int y = 0; y < 10; y++) { - final ui.Offset offset = ui.Offset( - padding + (tileSize + padding) * x, - padding + (tileSize + padding) * y, - ); - sceneBuilder.pushOffset(offset.dx, offset.dy); - sceneBuilder.addPicture( - ui.Offset.zero, - StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize, tileSize)), - ); - sceneBuilder.addPlatformView( - 1, - offset: const ui.Offset(5, 5), - width: tileSize - 10, - height: tileSize - 10, - ); - sceneBuilder.addPicture( - const ui.Offset(10, 10), - StubPicture(const ui.Rect.fromLTWH(0, 0, tileSize - 20, tileSize - 20)), - ); - sceneBuilder.pop(); - expectedPlatformViews.add( - PlatformView( - 1, - const ui.Rect.fromLTRB(5.0, 5.0, tileSize - 5.0, tileSize - 5.0), - PlatformViewStyling(position: PlatformViewPosition.offset(offset)), - ), - ); - } - } - - final EngineScene scene = sceneBuilder.build() as EngineScene; - final List slices = scene.rootLayer.slices; - - // It is important that the optimizations of the scene builder result in - // there only being two scene slices. - expect(slices.length, 2); - expect( - slices[0], - layerSlice( - withPictureRect: const ui.Rect.fromLTRB( - padding, - padding, - 10 * (padding + tileSize), - 10 * (padding + tileSize), - ), - withPlatformViews: expectedPlatformViews, - ), - ); - expect( - slices[1], - layerSlice( - withPictureRect: const ui.Rect.fromLTRB( - padding + 10, - padding + 10, - 10 * (padding + tileSize) - 10, - 10 * (padding + tileSize) - 10, - ), - ), - ); - }); - }); -} - -LayerSliceMatcher layerSlice({ - ui.Rect withPictureRect = ui.Rect.zero, - List withPlatformViews = const [], -}) => LayerSliceMatcher(withPictureRect, withPlatformViews); - -class LayerSliceMatcher extends Matcher { - LayerSliceMatcher(this.expectedPictureRect, this.expectedPlatformViews); - - final ui.Rect expectedPictureRect; - final List expectedPlatformViews; - - @override - Description describe(Description description) { - return description.add( - '', - ); - } - - @override - bool matches(dynamic item, Map matchState) { - if (item is! LayerSlice) { - return false; - } - final ScenePicture picture = item.picture; - if (picture is! StubPicture) { - return false; - } - - if (picture.cullRect != expectedPictureRect) { - return false; - } - - if (item.platformViews.length != expectedPlatformViews.length) { - return false; - } - - for (int i = 0; i < item.platformViews.length; i++) { - final PlatformView expectedView = expectedPlatformViews[i]; - final PlatformView actualView = item.platformViews[i]; - if (expectedView.viewId != actualView.viewId) { - return false; - } - if (expectedView.bounds != actualView.bounds) { - return false; - } - if (expectedView.styling != actualView.styling) { - return false; - } - } - - return true; - } -} diff --git a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart b/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart deleted file mode 100644 index 9d78ea7e12c..00000000000 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_builder_utils.dart +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -class StubPicture implements ScenePicture { - StubPicture(this.cullRect); - - bool _isDisposed = false; - - @override - final ui.Rect cullRect; - - @override - int get approximateBytesUsed => throw UnimplementedError(); - - @override - bool get debugDisposed => _isDisposed; - - @override - void dispose() { - assert(!_isDisposed); - _isDisposed = true; - } - - @override - Future toImage(int width, int height) { - throw UnimplementedError(); - } - - @override - ui.Image toImageSync(int width, int height) { - throw UnimplementedError(); - } -} - -class StubCompositePicture extends StubPicture { - StubCompositePicture(this.children) - : super( - children.fold(null, (ui.Rect? previousValue, StubPicture child) { - final ui.Rect childRect = child.cullRect; - if (childRect.isEmpty) { - return previousValue; - } - return previousValue?.expandToInclude(child.cullRect) ?? child.cullRect; - }) ?? - ui.Rect.zero, - ); - - final List children; -} - -class StubPictureRecorder implements ui.PictureRecorder { - StubPictureRecorder(this.canvas); - - final StubSceneCanvas canvas; - - @override - ui.Picture endRecording() { - return StubCompositePicture(canvas.pictures); - } - - @override - bool get isRecording => throw UnimplementedError(); -} - -class StubSceneCanvas implements SceneCanvas { - List pictures = []; - - // We actually use offsets in some of the tests, so we need to track the - // translate calls as they are made. - List offsetStack = [ui.Offset.zero]; - - ui.Offset get currentOffset { - return offsetStack.last; - } - - set currentOffset(ui.Offset offset) { - offsetStack[offsetStack.length - 1] = offset; - } - - @override - void drawPicture(ui.Picture picture) { - pictures.add(StubPicture((picture as StubPicture).cullRect.shift(currentOffset))); - } - - @override - void clipPath(ui.Path path, {bool doAntiAlias = true}) {} - - @override - void clipRRect(ui.RRect rrect, {bool doAntiAlias = true}) {} - - @override - void clipRSuperellipse(ui.RSuperellipse rsuperellipse, {bool doAntiAlias = true}) {} - - @override - void clipRect(ui.Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {} - - @override - void drawArc( - ui.Rect rect, - double startAngle, - double sweepAngle, - bool useCenter, - ui.Paint paint, - ) {} - - @override - void drawAtlas( - ui.Image atlas, - List transforms, - List rects, - List? colors, - ui.BlendMode? blendMode, - ui.Rect? cullRect, - ui.Paint paint, - ) {} - - @override - void drawCircle(ui.Offset c, double radius, ui.Paint paint) {} - - @override - void drawColor(ui.Color color, ui.BlendMode blendMode) {} - - @override - void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) {} - - @override - void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) {} - - @override - void drawImageNine(ui.Image image, ui.Rect center, ui.Rect dst, ui.Paint paint) {} - - @override - void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) {} - - @override - void drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) {} - - @override - void drawOval(ui.Rect rect, ui.Paint paint) {} - - @override - void drawPaint(ui.Paint paint) {} - - @override - void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {} - - @override - void drawPath(ui.Path path, ui.Paint paint) {} - - @override - void drawPoints(ui.PointMode pointMode, List points, ui.Paint paint) {} - - @override - void drawRRect(ui.RRect rrect, ui.Paint paint) {} - - @override - void drawRSuperellipse(ui.RSuperellipse rsuperellipse, ui.Paint paint) {} - - @override - void drawRawAtlas( - ui.Image atlas, - Float32List rstTransforms, - Float32List rects, - Int32List? colors, - ui.BlendMode? blendMode, - ui.Rect? cullRect, - ui.Paint paint, - ) {} - - @override - void drawRawPoints(ui.PointMode pointMode, Float32List points, ui.Paint paint) {} - - @override - void drawRect(ui.Rect rect, ui.Paint paint) {} - - @override - void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) {} - - @override - void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) {} - - @override - ui.Rect getDestinationClipBounds() { - throw UnimplementedError(); - } - - @override - ui.Rect getLocalClipBounds() { - throw UnimplementedError(); - } - - @override - int getSaveCount() { - throw UnimplementedError(); - } - - @override - Float64List getTransform() { - throw UnimplementedError(); - } - - @override - void restore() { - offsetStack.removeLast(); - } - - @override - void restoreToCount(int count) {} - - @override - void rotate(double radians) {} - - @override - void save() { - offsetStack.add(currentOffset); - } - - @override - void saveLayer(ui.Rect? bounds, ui.Paint paint) {} - - @override - void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter backdropFilter) {} - - @override - void scale(double sx, [double? sy]) {} - - @override - void skew(double sx, double sy) {} - - @override - void transform(Float64List matrix4) {} - - @override - void translate(double dx, double dy) { - currentOffset += ui.Offset(dx, dy); - } -} 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 deleted file mode 100644 index 23b8cd5fd34..00000000000 --- a/engine/src/flutter/lib/web_ui/test/engine/scene_view_test.dart +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; - -import 'package:ui/ui.dart' as ui; -import 'package:ui/ui_web/src/ui_web.dart'; - -import '../common/test_initialization.dart'; -import 'scene_builder_utils.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -class StubPictureRenderer implements PictureRenderer { - final DomHTMLCanvasElement scratchCanvasElement = createDomCanvasElement(width: 500, height: 500); - - @override - Future renderPictures(List pictures, int width, int height) async { - renderedPictures.addAll(pictures); - final List bitmaps = await Future.wait( - pictures.map((ScenePicture picture) { - final ui.Rect cullRect = picture.cullRect; - final Future bitmap = createImageBitmap(scratchCanvasElement, ( - x: 0, - y: 0, - width: cullRect.width.toInt(), - height: cullRect.height.toInt(), - )); - return bitmap; - }), - ); - return (imageBitmaps: bitmaps, rasterStartMicros: 0, rasterEndMicros: 0); - } - - List renderedPictures = []; - List clippedPictures = []; -} - -class StubFlutterView implements EngineFlutterView { - // Overridden in some tests - @override - DomManager dom = StubDomManager(); - - @override - double get devicePixelRatio => throw UnimplementedError(); - - @override - EngineFlutterDisplay get display => throw UnimplementedError(); - - @override - List get displayFeatures => throw UnimplementedError(); - - @override - ui.GestureSettings get gestureSettings => throw UnimplementedError(); - - @override - ViewPadding get padding => throw UnimplementedError(); - - @override - ViewConstraints get physicalConstraints => throw UnimplementedError(); - - @override - ui.Size get physicalSize => const ui.Size(1000, 1000); - - @override - EnginePlatformDispatcher get platformDispatcher => throw UnimplementedError(); - - @override - void render(ui.Scene scene, {ui.Size? size}) {} - - @override - ViewPadding get systemGestureInsets => throw UnimplementedError(); - - @override - void updateSemantics(ui.SemanticsUpdate update) {} - - @override - int get viewId => throw UnimplementedError(); - - @override - ViewPadding get viewInsets => throw UnimplementedError(); - - @override - ViewPadding get viewPadding => throw UnimplementedError(); - - @override - ui.Size? debugPhysicalSizeOverride; - - @override - bool isDisposed = false; - - @override - PointerBinding get pointerBinding => throw UnimplementedError(); - - @override - set pointerBinding(_) { - throw UnimplementedError(); - } - - @override - ContextMenu get contextMenu => throw UnimplementedError(); - - @override - void debugForceResize() { - throw UnimplementedError(); - } - - @override - DimensionsProvider get dimensionsProvider => throw UnimplementedError(); - - @override - void dispose() { - throw UnimplementedError(); - } - - @override - EmbeddingStrategy get embeddingStrategy => throw UnimplementedError(); - - @override - MouseCursor get mouseCursor => throw UnimplementedError(); - - @override - Stream get onResize => throw UnimplementedError(); - - @override - void resize(ui.Size newPhysicalSize) { - throw UnimplementedError(); - } - - @override - EngineSemanticsOwner get semantics => throw UnimplementedError(); -} - -class StubPath implements ScenePath { - StubPath(this.pathString, this.transformedPathString); - - final String pathString; - final String transformedPathString; - Float64List? appliedTransform; - - @override - ui.PathFillType get fillType => throw UnimplementedError(); - - @override - set fillType(ui.PathFillType value) => throw UnimplementedError(); - - @override - void moveTo(double x, double y) => throw UnimplementedError(); - - @override - void relativeMoveTo(double dx, double dy) => throw UnimplementedError(); - - @override - void lineTo(double x, double y) => throw UnimplementedError(); - - @override - void relativeLineTo(double dx, double dy) => throw UnimplementedError(); - - @override - void quadraticBezierTo(double x1, double y1, double x2, double y2) => throw UnimplementedError(); - - @override - void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) => - throw UnimplementedError(); - - @override - void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) => - throw UnimplementedError(); - - @override - void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) => - throw UnimplementedError(); - - @override - void conicTo(double x1, double y1, double x2, double y2, double w) => throw UnimplementedError(); - - @override - void relativeConicTo(double x1, double y1, double x2, double y2, double w) => - throw UnimplementedError(); - - @override - void arcTo(ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) => - throw UnimplementedError(); - - @override - void arcToPoint( - ui.Offset arcEnd, { - ui.Radius radius = ui.Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }) => throw UnimplementedError(); - - @override - void relativeArcToPoint( - ui.Offset arcEndDelta, { - ui.Radius radius = ui.Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }) => throw UnimplementedError(); - - @override - void addRect(ui.Rect rect) => throw UnimplementedError(); - - @override - void addOval(ui.Rect oval) => throw UnimplementedError(); - - @override - void addArc(ui.Rect oval, double startAngle, double sweepAngle) => throw UnimplementedError(); - - @override - void addPolygon(List points, bool close) => throw UnimplementedError(); - - @override - void addRRect(ui.RRect rrect) => throw UnimplementedError(); - - @override - void addRSuperellipse(ui.RSuperellipse rsuperellipse) => throw UnimplementedError(); - - @override - void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) => - throw UnimplementedError(); - - @override - void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) => - throw UnimplementedError(); - - @override - void close() => throw UnimplementedError(); - - @override - void reset() => throw UnimplementedError(); - - @override - bool contains(ui.Offset point) => throw UnimplementedError(); - - @override - ui.Path shift(ui.Offset offset) => throw UnimplementedError(); - - @override - StubPath transform(Float64List matrix4) { - appliedTransform = matrix4; - return StubPath(transformedPathString, ''); - } - - @override - ui.Rect getBounds() => throw UnimplementedError(); - - @override - ui.PathMetrics computeMetrics({bool forceClosed = false}) => throw UnimplementedError(); - - @override - String toSvgString() => pathString; -} - -void testMain() { - late EngineSceneView sceneView; - late StubPictureRenderer stubPictureRenderer; - - setUpImplicitView(); - - setUp(() { - stubPictureRenderer = StubPictureRenderer(); - sceneView = EngineSceneView(stubPictureRenderer, StubFlutterView()); - }); - - tearDown(() { - expect( - stubPictureRenderer.clippedPictures.every((StubPicture picture) => picture.debugDisposed), - true, - ); - }); - - test('SceneView places platform view according to device-pixel ratio', () async { - debugOverrideDevicePixelRatio(2.0); - - final StubPath clipPath = StubPath('M 2 2', 'M 1 1'); - - final PlatformView platformView = PlatformView( - 1, - const ui.Rect.fromLTWH(50, 80, 100, 120), - PlatformViewStyling(clip: PlatformViewPathClip(clipPath)), - ); - final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), [platformView])); - final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene, null); - - final DomElement sceneElement = sceneView.sceneElement; - final List children = sceneElement.children.toList(); - - expect(children.length, 1); - final DomElement clipElement = children.first; - expect(clipElement.tagName, equalsIgnoringCase('flt-clip')); - - expect(clipElement.style.clipPath, 'path("M 1 1")'); - - // We expect the path to be scaled down by 1 / DPR - final Matrix4 expectedTransform = Matrix4.identity()..scale(0.5); - expect(clipPath.appliedTransform, expectedTransform.toFloat64()); - - final List clipChildren = clipElement.children.toList(); - expect(clipChildren.length, 1); - - final DomElement containerElement = clipChildren.first; - final DomCSSStyleDeclaration style = containerElement.style; - expect(style.left, ''); - expect(style.top, ''); - expect(style.width, '100px'); - expect(style.height, '120px'); - - // The heavy lifting of offsetting and sizing is done by the transform - expect(style.transform, 'matrix(0.5, 0, 0, 0.5, 25, 40)'); - - debugOverrideDevicePixelRatio(null); - }); - - test('SceneView always renders most recent picture and skips intermediate pictures', () async { - final List pictures = []; - final List> renderFutures = >[]; - for (int i = 1; i < 20; i++) { - final StubPicture picture = StubPicture(const ui.Rect.fromLTWH(50, 80, 100, 120)); - pictures.add(picture); - final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(LayerSlice(picture, [])); - final EngineScene scene = EngineScene(rootLayer); - renderFutures.add(sceneView.renderScene(scene, null)); - } - await Future.wait(renderFutures); - - // Should just render the first and last pictures and skip the one inbetween. - expect(stubPictureRenderer.renderedPictures.length, 2); - 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(LayerSlice(picture, [])); - final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene, null); - - expect(stubPictureRenderer.renderedPictures.length, 1); - }); - - test('SceneView places platform view contents in the DOM', () async { - const int expectedPlatformViewId = 1234; - - int? injectedViewId; - final DomManager stubDomManager = StubDomManager() - ..injectPlatformViewOverride = (int viewId) { - injectedViewId = viewId; - }; - sceneView = EngineSceneView(stubPictureRenderer, StubFlutterView()..dom = stubDomManager); - - final PlatformView platformView = PlatformView( - expectedPlatformViewId, - const ui.Rect.fromLTWH(50, 80, 100, 120), - const PlatformViewStyling(), - ); - - final EngineRootLayer rootLayer = EngineRootLayer(); - rootLayer.slices.add(LayerSlice(StubPicture(ui.Rect.zero), [platformView])); - final EngineScene scene = EngineScene(rootLayer); - await sceneView.renderScene(scene, null); - - expect( - injectedViewId, - expectedPlatformViewId, - reason: 'SceneView should call injectPlatformView on its flutterView.dom', - ); - }); -} - -class StubDomManager implements DomManager { - void Function(int platformViewId) injectPlatformViewOverride = (int id) {}; - @override - void injectPlatformView(int platformViewId) { - injectPlatformViewOverride(platformViewId); - } - - @override - DomElement get platformViewsHost => throw UnimplementedError(); - - @override - DomShadowRoot get renderingHost => throw UnimplementedError(); - - @override - DomElement get rootElement => throw UnimplementedError(); - - @override - DomElement get sceneHost => throw UnimplementedError(); - - @override - DomElement get semanticsHost => throw UnimplementedError(); - - @override - void setScene(DomElement sceneElement) {} - - @override - DomElement get textEditingHost => throw UnimplementedError(); -} diff --git a/engine/src/flutter/lib/web_ui/test/ui/canvas_golden_test.dart b/engine/src/flutter/lib/web_ui/test/ui/canvas_golden_test.dart index 7ecf5f5156c..9a72ff059a2 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/canvas_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/canvas_golden_test.dart @@ -38,9 +38,7 @@ void testMain() { await drawTestPicture(canvas as LayerCanvas); await drawPictureUsingCurrentRenderer(recorder.endRecording()); await matchGoldenFile('ui_weakref_picture.png', region: kDefaultRegion); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('text style - foreground/background/color do not leak across paragraphs', () async { const double testWidth = 440; diff --git a/engine/src/flutter/lib/web_ui/test/ui/color_filter_golden_test.dart b/engine/src/flutter/lib/web_ui/test/ui/color_filter_golden_test.dart index 396b4b21dd9..d167b03da7f 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/color_filter_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/color_filter_golden_test.dart @@ -10,7 +10,6 @@ import 'package:web_engine_tester/golden_tester.dart'; import '../common/rendering.dart'; import '../common/test_initialization.dart'; -import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -292,9 +291,7 @@ void testMain() { await renderScene(builder.build()); await matchGoldenFile('ui_colorfilter_as_imagefilter.png', region: region); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm || isSafari || isFirefox); + }); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520 }, skip: isSafari || isFirefox); } diff --git a/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart b/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart index 236bda9d241..80c6e1f0120 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/filters_test.dart @@ -486,8 +486,8 @@ Future testMain() async { await renderScene(builder.build()); await matchGoldenFile('ui_zero_sigma_blur.png', region: region); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 + // Skwasm crashes with a zero-sigma blur. See: + // https://github.com/flutter/flutter/issues/174583 }, skip: isSkwasm); test('== operator', () { diff --git a/engine/src/flutter/lib/web_ui/test/ui/layer_test.dart b/engine/src/flutter/lib/web_ui/test/ui/layer_test.dart index c2eedf31f34..5764d5abfd1 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/layer_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/layer_test.dart @@ -135,7 +135,5 @@ void testMain() { region: const ui.Rect.fromLTRB(0, 0, 200, 200), ); }); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); } diff --git a/engine/src/flutter/lib/web_ui/test/ui/multi_view_test.dart b/engine/src/flutter/lib/web_ui/test/ui/multi_view_test.dart index eb1dd2751ff..861bffe475b 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/multi_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/multi_view_test.dart @@ -54,9 +54,7 @@ void testMain() { createDomElement('unregistered-view'), ); expect(() => renderer.renderScene(scene, unregisteredView), throwsAssertionError); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('will dispose the Rasterizer for a disposed view', () async { final EngineFlutterView view = EngineFlutterView( diff --git a/engine/src/flutter/lib/web_ui/test/ui/picture_test.dart b/engine/src/flutter/lib/web_ui/test/ui/picture_test.dart index 3acf358c39d..ce7ebf5df6c 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/picture_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/picture_test.dart @@ -8,7 +8,6 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import '../common/test_initialization.dart'; -import 'utils.dart'; void main() { internalBootstrapBrowserTest(() => testMain); @@ -33,9 +32,7 @@ Future testMain() async { picture.dispose(); expect(picture.debugDisposed, true); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('Picture construction invokes onCreate once', () async { int onCreateInvokedCount = 0; diff --git a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart index ecd6ddcfec2..d5a4609d73e 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart @@ -483,9 +483,7 @@ Future testMain() async { 'matrix(1, 0, 0, 1, 6, 6)', 'matrix(1, 0, 0, 1, 3, 3)', ]); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('converts device pixels to logical pixels (no clips)', () async { EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4); @@ -503,9 +501,7 @@ Future testMain() async { expect(getTransformChain(slotHost), ['matrix(0.25, 0, 0, 0.25, 1.5, 1.5)']); EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('converts device pixels to logical pixels (with clips)', () async { EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4); @@ -529,9 +525,7 @@ Future testMain() async { 'matrix(0.25, 0, 0, 0.25, 0.75, 0.75)', ]); EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('renders overlays on top of platform views', () async { debugOverrideJsConfiguration( @@ -677,9 +671,7 @@ Future testMain() async { await renderTestScene(viewCount: 0); _expectSceneMatches(<_EmbeddedViewMarker>[]); debugOverrideJsConfiguration(null); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('correctly reuses overlays', () async { final ui.PictureRecorder testRecorder = ui.PictureRecorder(); @@ -803,9 +795,7 @@ Future testMain() async { _overlay, _platformView, ]); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('embeds and disposes of a platform view', () async { await createPlatformView(1, platformViewType); @@ -899,10 +889,8 @@ Future testMain() async { implicitView.debugForceResize(); // ImageDecoder is not supported in Safari or Firefox. - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 }, - skip: isSafari || isFirefox || isSkwasm, + skip: isSafari || isFirefox, ); test('does not crash when a prerolled platform view is not composited', () async { @@ -916,9 +904,7 @@ Future testMain() async { // The below line should not throw an error. await renderScene(sb.build()); _expectSceneMatches(<_EmbeddedViewMarker>[]); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('does not create overlays for invisible platform views', () async { final ui.PictureRecorder testRecorder = ui.PictureRecorder(); @@ -1150,9 +1136,7 @@ Future testMain() async { _platformView, _overlay, ]); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('can dispose without crashing', () async { ui_web.platformViewRegistry.registerViewFactory( @@ -1181,9 +1165,7 @@ Future testMain() async { // The following line used to cause a "Concurrent modification during iteration" embedder.dispose(); }, returnsNormally); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('optimizes overlays when pictures and platform views do not overlap', () async { ui.Picture rectPicture(ui.Rect rect) { @@ -1422,10 +1404,7 @@ Future testMain() async { .map((CompositionCanvas canvas) => canvas.pictures.length) .toList(); expect(picturesPerCanvas, [1, 2]); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); - + }); test('can customize amount of overlays', () async { final ui.PictureRecorder testRecorder = ui.PictureRecorder(); final ui.Canvas testCanvas = ui.Canvas(testRecorder); @@ -1493,9 +1472,7 @@ Future testMain() async { ]); debugOverrideJsConfiguration(null); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); + }); test('correctly rearranges pictures to second-to-last canvas ' 'when hitting canvas limit', () async { @@ -1605,10 +1582,7 @@ Future testMain() async { .toList(); expect(picturesPerCanvasInSecondRendering, [19]); debugOverrideJsConfiguration(null); - // Unskip when Skwasm and CanvasKit are unified: - // https://github.com/flutter/flutter/issues/172311 - }, skip: isSkwasm); - + }); test('disposes render pictures', () async { Instrumentation.enabled = true; Instrumentation.instance.debugCounters.clear();