From a59c8d3b9a84cff99a270d2a16cb76064c6f714c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen <1961493+harryterkelsen@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:55:09 -0700 Subject: [PATCH 1/4] Revert "Revert "Use a single OffscreenCanvas for rendering in CanvasKit" (#45744)" This reverts commit 153edff425826aafc6ff9d5c89b66675ac2e0db8. --- ci/licenses_golden/licenses_flutter | 6 +- lib/web_ui/lib/src/engine.dart | 3 +- .../src/engine/canvaskit/canvaskit_api.dart | 10 + .../src/engine/canvaskit/embedded_views.dart | 169 ++++------ .../lib/src/engine/canvaskit/picture.dart | 4 +- .../lib/src/engine/canvaskit/rasterizer.dart | 46 ++- .../src/engine/canvaskit/render_canvas.dart | 113 +++++++ .../canvaskit/render_canvas_factory.dart | 142 ++++++++ .../lib/src/engine/canvaskit/surface.dart | 310 +++++++++--------- .../src/engine/canvaskit/surface_factory.dart | 167 ---------- lib/web_ui/lib/src/engine/configuration.dart | 13 +- lib/web_ui/lib/src/engine/dom.dart | 120 ++++--- .../test/canvaskit/canvas_golden_test.dart | 2 +- lib/web_ui/test/canvaskit/common.dart | 2 +- .../test/canvaskit/embedded_views_test.dart | 179 ++-------- .../canvaskit/render_canvas_factory_test.dart | 95 ++++++ .../test/canvaskit/render_canvas_test.dart | 62 ++++ .../test/canvaskit/surface_factory_test.dart | 103 ------ lib/web_ui/test/canvaskit/surface_test.dart | 91 ++--- lib/web_ui/test/engine/scene_view_test.dart | 26 +- 20 files changed, 813 insertions(+), 850 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart create mode 100644 lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart delete mode 100644 lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart create mode 100644 lib/web_ui/test/canvaskit/render_canvas_factory_test.dart create mode 100644 lib/web_ui/test/canvaskit/render_canvas_test.dart delete mode 100644 lib/web_ui/test/canvaskit/surface_factory_test.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c01aba892418a..5d47269235ca8 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2619,10 +2619,11 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart + ../../../flutter/LICENSE @@ -5396,10 +5397,11 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index da7c3cdfddbe1..974411798aab5 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -43,10 +43,11 @@ export 'engine/canvaskit/picture.dart'; export 'engine/canvaskit/picture_recorder.dart'; export 'engine/canvaskit/raster_cache.dart'; export 'engine/canvaskit/rasterizer.dart'; +export 'engine/canvaskit/render_canvas.dart'; +export 'engine/canvaskit/render_canvas_factory.dart'; export 'engine/canvaskit/renderer.dart'; export 'engine/canvaskit/shader.dart'; export 'engine/canvaskit/surface.dart'; -export 'engine/canvaskit/surface_factory.dart'; export 'engine/canvaskit/text.dart'; export 'engine/canvaskit/text_fragmenter.dart'; export 'engine/canvaskit/util.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index f167a19c3be9c..dff531d06c244 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -161,6 +161,13 @@ extension CanvasKitExtension on CanvasKit { DomCanvasElement canvas, SkWebGLContextOptions options) => _GetWebGLContext(canvas, options).toDartDouble; + @JS('GetWebGLContext') + external JSNumber _GetOffscreenWebGLContext( + DomOffscreenCanvas canvas, SkWebGLContextOptions options); + double GetOffscreenWebGLContext( + DomOffscreenCanvas canvas, SkWebGLContextOptions options) => + _GetOffscreenWebGLContext(canvas, options).toDartDouble; + @JS('MakeGrContext') external SkGrContext _MakeGrContext(JSNumber glContext); SkGrContext MakeGrContext(double glContext) => @@ -199,6 +206,9 @@ extension CanvasKitExtension on CanvasKit { external SkSurface MakeSWCanvasSurface(DomCanvasElement canvas); + @JS('MakeSWCanvasSurface') + external SkSurface MakeOffscreenSWCanvasSurface(DomOffscreenCanvas canvas); + /// Creates an image from decoded pixels represented as a list of bytes. /// /// The pixel data must be encoded according to the image info in [info]. diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index e3274d9e365c3..57ff2ed6836ae 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -18,9 +18,9 @@ import 'embedded_views_diff.dart'; import 'path.dart'; import 'picture.dart'; import 'picture_recorder.dart'; +import 'render_canvas.dart'; +import 'render_canvas_factory.dart'; import 'renderer.dart'; -import 'surface.dart'; -import 'surface_factory.dart'; /// This composites HTML views into the [ui.Scene]. class HtmlViewEmbedder { @@ -31,42 +31,6 @@ class HtmlViewEmbedder { DomElement get skiaSceneHost => CanvasKitRenderer.instance.sceneHost!; - /// Force the view embedder to disable overlays. - /// - /// This should never be used outside of tests. - static set debugDisableOverlays(bool disable) { - // Short circuit if the value is the same as what we already have. - if (disable == _debugOverlaysDisabled) { - return; - } - _debugOverlaysDisabled = disable; - final SurfaceFactory? instance = SurfaceFactory.debugUninitializedInstance; - if (instance != null) { - instance.releaseSurfaces(); - instance.removeSurfacesFromDom(); - instance.debugClear(); - } - if (disable) { - // If we are disabling overlays then get the current [SurfaceFactory] - // instance, clear it, and overwrite it with a new instance with only - // one surface for the base surface. - SurfaceFactory.debugSetInstance(SurfaceFactory(1)); - } else { - // If we are re-enabling overlays then replace the current - // [SurfaceFactory]instance with one with - // [configuration.canvasKitMaximumSurfaces] overlays. - SurfaceFactory.debugSetInstance( - SurfaceFactory(configuration.canvasKitMaximumSurfaces)); - } - } - - static bool _debugOverlaysDisabled = false; - - /// Whether or not we have issues a warning to the user about having too many - /// surfaces on screen at once. This is so we only warn once, instead of every - /// frame. - bool _warnedAboutTooManySurfaces = false; - /// The context for the current frame. EmbedderFrameContext _context = EmbedderFrameContext(); @@ -86,10 +50,12 @@ class HtmlViewEmbedder { /// * The number of clipping elements used last time the view was composited. final Map _viewClipChains = {}; - /// Surfaces used to draw on top of platform views, keyed by platform view ID. - /// - /// These surfaces are cached in the [OverlayCache] and reused. - final Map _overlays = {}; + /// The maximum number of overlays to create. Too many overlays can cause a + /// performance burden. + static const int maximumOverlays = 7; + + /// Canvases used to draw on top of platform views, keyed by platform view ID. + final Map _overlays = {}; /// The views that need to be recomposited into the scene on the next frame. final Set _viewsToRecomposite = {}; @@ -100,6 +66,9 @@ class HtmlViewEmbedder { /// The most recent composition order. final List _activeCompositionOrder = []; + /// The most recent overlay groups. + List _activeOverlayGroups = []; + /// The size of the frame, in physical pixels. ui.Size _frameSize = ui.window.physicalSize; @@ -124,20 +93,10 @@ class HtmlViewEmbedder { } void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { - final bool hasAvailableOverlay = - _context.pictureRecordersCreatedDuringPreroll.length < - SurfaceFactory.instance.maximumOverlays; - if (!hasAvailableOverlay && !_warnedAboutTooManySurfaces) { - _warnedAboutTooManySurfaces = true; - printWarning('Flutter was unable to create enough overlay surfaces. ' - 'This is usually caused by too many platform views being ' - 'displayed at once. ' - 'You may experience incorrect rendering.'); - } // We need an overlay for each visible platform view. Invisible platform // views will be grouped with (at most) one visible platform view later. final bool needNewOverlay = PlatformViewManager.instance.isVisible(viewId); - if (needNewOverlay && hasAvailableOverlay) { + if (needNewOverlay) { final CkPictureRecorder pictureRecorder = CkPictureRecorder(); pictureRecorder.beginRecording(ui.Offset.zero & _frameSize); _context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder); @@ -409,26 +368,27 @@ class HtmlViewEmbedder { (_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty) ? null : diffViewList(_activeCompositionOrder, _compositionOrder); - _updateOverlays(diffResult); + final List? overlayGroups = _updateOverlays(diffResult); + if (overlayGroups != null) { + _activeOverlayGroups = overlayGroups; + } assert( - _context.pictureRecorders.length == _overlays.length, - 'There should be the same number of picture recorders ' + _context.pictureRecorders.length >= _overlays.length, + 'There should be at least as many picture recorders ' '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).', ); - int pictureRecorderIndex = 0; - for (int i = 0; i < _compositionOrder.length; i++) { - final int viewId = _compositionOrder[i]; - if (_overlays[viewId] != null) { - final SurfaceFrame frame = _overlays[viewId]!.acquireFrame(_frameSize); - final CkCanvas canvas = frame.skiaCanvas; - final CkPicture ckPicture = - _context.pictureRecorders[pictureRecorderIndex].endRecording(); - canvas.clear(const ui.Color(0x00000000)); - canvas.drawPicture(ckPicture); + int pictureRecorderIndex = 0; + for (final OverlayGroup overlayGroup in _activeOverlayGroups) { + final RenderCanvas overlay = _overlays[overlayGroup.last]!; + final List pictures = []; + for (int i = 0; i < overlayGroup.visibleCount; i++) { + pictures.add( + _context.pictureRecorders[pictureRecorderIndex].endRecording()); pictureRecorderIndex++; - frame.submit(); } + CanvasKitRenderer.instance.rasterizer + .rasterizeToCanvas(overlay, pictures); } for (final CkPictureRecorder recorder in _context.pictureRecordersCreatedDuringPreroll) { @@ -481,7 +441,7 @@ class HtmlViewEmbedder { if (diffResult.addToBeginning) { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.insertBefore(platformViewRoot, elementToInsertBefore); - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.insertBefore( overlay.htmlElement, elementToInsertBefore); @@ -489,7 +449,7 @@ class HtmlViewEmbedder { } else { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.append(platformViewRoot); - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); } @@ -514,7 +474,7 @@ class HtmlViewEmbedder { } } } else { - SurfaceFactory.instance.removeSurfacesFromDom(); + RenderCanvasFactory.instance.removeSurfacesFromDom(); for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; @@ -532,7 +492,7 @@ class HtmlViewEmbedder { } final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; skiaSceneHost.append(platformViewRoot); if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); @@ -568,8 +528,8 @@ class HtmlViewEmbedder { void _releaseOverlay(int viewId) { if (_overlays[viewId] != null) { - final Surface overlay = _overlays[viewId]!; - SurfaceFactory.instance.releaseSurface(overlay); + final RenderCanvas overlay = _overlays[viewId]!; + RenderCanvasFactory.instance.releaseCanvas(overlay); _overlays.remove(viewId); } } @@ -591,13 +551,13 @@ class HtmlViewEmbedder { // composition order of the current and previous frame, respectively. // // TODO(hterkelsen): Test this more thoroughly. - void _updateOverlays(ViewListDiffResult? diffResult) { + List? _updateOverlays(ViewListDiffResult? diffResult) { if (diffResult != null && diffResult.viewsToAdd.isEmpty && diffResult.viewsToRemove.isEmpty) { // The composition order has not changed, continue using the assigned // overlays. - return; + return null; } // Group platform views from their composition order. // Each group contains one visible view, and any number of invisible views @@ -606,17 +566,10 @@ class HtmlViewEmbedder { getOverlayGroups(_compositionOrder); final List viewsNeedingOverlays = overlayGroups.map((OverlayGroup group) => group.last).toList(); - // If there were more visible views than overlays, then the last group - // doesn't have an overlay. - if (viewsNeedingOverlays.length > SurfaceFactory.instance.maximumOverlays) { - assert(viewsNeedingOverlays.length == - SurfaceFactory.instance.maximumOverlays + 1); - viewsNeedingOverlays.removeLast(); - } if (diffResult == null) { // Everything is going to be explicitly recomposited anyway. Release all // the surfaces and assign an overlay to all the surfaces needing one. - SurfaceFactory.instance.releaseSurfaces(); + RenderCanvasFactory.instance.releaseCanvases(); _overlays.clear(); viewsNeedingOverlays.forEach(_initializeOverlay); } else { @@ -635,6 +588,7 @@ class HtmlViewEmbedder { .forEach(_initializeOverlay); } assert(_overlays.length == viewsNeedingOverlays.length); + return overlayGroups; } // Group the platform views into "overlay groups". These are sublists @@ -646,12 +600,8 @@ class HtmlViewEmbedder { // be assigned an overlay are grouped together and will be rendered on top of // the rest of the scene. List getOverlayGroups(List views) { - final int maxOverlays = SurfaceFactory.instance.maximumOverlays; - if (maxOverlays == 0) { - return const []; - } final List result = []; - OverlayGroup currentGroup = OverlayGroup([]); + OverlayGroup currentGroup = OverlayGroup(); for (int i = 0; i < views.length; i++) { final int view = views[i]; @@ -660,8 +610,10 @@ class HtmlViewEmbedder { currentGroup.add(view); } else { // `view` is visible. - if (!currentGroup.hasVisibleView) { - // If `view` is the first visible one of the group, add it. + if (!currentGroup.hasVisibleView || + result.length + 1 >= HtmlViewEmbedder.maximumOverlays) { + // If `view` is the first visible one of the group or we've reached + // the maximum number of overlays, add it. currentGroup.add(view, visible: true); } else { // There's already a visible `view` in `currentGroup`, so a new @@ -671,17 +623,8 @@ class HtmlViewEmbedder { // We only care about groups that have one visible view. result.add(currentGroup); } - // If there are overlays still available. - if (result.length < maxOverlays) { - // Create a new group, starting with `view`. - currentGroup = OverlayGroup([view], visible: true); - } else { - // Add the rest of the views to a final group that will be rendered - // on top of the scene. - currentGroup = OverlayGroup(views.sublist(i), visible: true); - // And break out of the loop! - break; - } + currentGroup = OverlayGroup(); + currentGroup.add(view, visible: true); } } } @@ -696,8 +639,7 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final Surface overlay = SurfaceFactory.instance.getSurface()!; - overlay.createOrUpdateSurface(_frameSize); + final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas(); _overlays[viewId] = overlay; } @@ -742,29 +684,30 @@ class HtmlViewEmbedder { /// Every overlay group is a list containing a visible view preceded or followed /// by zero or more invisible views. class OverlayGroup { - /// Constructor - OverlayGroup( - List viewGroup, { - bool visible = false, - }) : _group = viewGroup, - _containsVisibleView = visible; + OverlayGroup() : _group = []; // The internal list of ints. final List _group; - // A boolean flag to mark if any visible view has been added to the list. - bool _containsVisibleView; + + /// The number of visible views in this group. + int _visibleCount = 0; /// Add a [view] (maybe [visible]) to this group. void add(int view, {bool visible = false}) { _group.add(view); - _containsVisibleView |= visible; + if (visible) { + _visibleCount++; + } } /// Get the "last" view added to this group. int get last => _group.last; /// Returns true if this group contains any visible view. - bool get hasVisibleView => _group.isNotEmpty && _containsVisibleView; + bool get hasVisibleView => _visibleCount > 0; + + /// Returns the number of visible views in this overlay group. + int get visibleCount => _visibleCount; } /// Represents a Clip Chain (for a view). diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 52b8229ec2b9f..668afeeb47434 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -11,8 +11,8 @@ import 'canvas.dart'; import 'canvaskit_api.dart'; import 'image.dart'; import 'native_memory.dart'; +import 'render_canvas_factory.dart'; import 'surface.dart'; -import 'surface_factory.dart'; /// Implements [ui.Picture] on top of [SkPicture]. class CkPicture implements ScenePicture { @@ -99,7 +99,7 @@ class CkPicture implements ScenePicture { CkImage toImageSync(int width, int height) { assert(debugCheckNotDisposed('Cannot convert picture to image.')); - final Surface surface = SurfaceFactory.instance.pictureToImageSurface; + final Surface surface = RenderCanvasFactory.instance.pictureToImageSurface; final CkSurface ckSurface = surface .createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble())); final CkCanvas ckCanvas = ckSurface.getCanvas(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 67f01fff852f9..8d150ecae4144 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -3,22 +3,29 @@ // found in the LICENSE file. import 'package:meta/meta.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../frame_reference.dart'; -import 'canvas.dart'; -import 'embedded_views.dart'; -import 'layer_tree.dart'; -import 'surface.dart'; -import 'surface_factory.dart'; - /// A class that can rasterize [LayerTree]s into a given [Surface]. class Rasterizer { final CompositorContext context = CompositorContext(); final List _postFrameCallbacks = []; + /// This is an SkSurface backed by an OffScreenCanvas. This single Surface is + /// used to render to many RenderCanvases to produce the rendered scene. + final Surface _offscreenSurface = Surface(); + ui.Size _currentFrameSize = ui.Size.zero; + + /// Render the given [pictures] so it is displayed by the given [canvas]. + Future rasterizeToCanvas( + RenderCanvas canvas, List pictures) async { + await _offscreenSurface.rasterizeToCanvas( + _currentFrameSize, canvas, pictures); + } + + /// Sets the maximum size of the Skia resource cache, in bytes. void setSkiaResourceCacheMaxBytes(int bytes) => - SurfaceFactory.instance.baseSurface.setSkiaResourceCacheMaxBytes(bytes); + _offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -29,17 +36,22 @@ class Rasterizer { return; } - final SurfaceFrame frame = - SurfaceFactory.instance.baseSurface.acquireFrame(layerTree.frameSize); - HtmlViewEmbedder.instance.frameSize = layerTree.frameSize; - final CkCanvas canvas = frame.skiaCanvas; - canvas.clear(const ui.Color(0x00000000)); - final Frame compositorFrame = - context.acquireFrame(canvas, HtmlViewEmbedder.instance); + _currentFrameSize = layerTree.frameSize; + _offscreenSurface.acquireFrame(_currentFrameSize); + HtmlViewEmbedder.instance.frameSize = _currentFrameSize; + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); + pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); + final Frame compositorFrame = context.acquireFrame( + pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance); compositorFrame.raster(layerTree, ignoreRasterCache: true); - SurfaceFactory.instance.baseSurface.addToScene(); - frame.submit(); + + CanvasKitRenderer.instance.sceneHost! + .prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement); + rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, + [pictureRecorder.endRecording()]); + HtmlViewEmbedder.instance.submitFrame(); } finally { _runPostFrameCallbacks(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart new file mode 100644 index 0000000000000..7793cfbe6cbce --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -0,0 +1,113 @@ +// 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:js_interop'; + +import 'package:ui/ui.dart' as ui; + +import '../dom.dart'; +import '../window.dart'; + +/// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit +/// in the (off-screen) SkSurface which is backed by an OffscreenCanvas. +/// +/// In a typical frame, the content will be rendered via CanvasKit in an +/// OffscreenCanvas, and then the contents will be transferred to the +/// RenderCanvas via `transferFromImageBitmap()`. +/// +/// If we need more RenderCanvases, for example in the case where there are +/// platform views and we need overlays to render the frame correctly, then +/// we will create multiple RenderCanvases, but crucially still only have +/// one OffscreenCanvas which transfers bitmaps to all of the RenderCanvases. +/// +/// To render into the OffscreenCanvas with CanvasKit we need to create a +/// WebGL context, which is not only expensive, but the browser has a limit +/// on the maximum amount of WebGL contexts which can be live at once. Using +/// a single OffscreenCanvas and multiple RenderCanvases allows us to only +/// create a single WebGL context. +class RenderCanvas { + RenderCanvas() { + canvasElement.setAttribute('aria-hidden', 'true'); + canvasElement.style.position = 'absolute'; + _updateLogicalHtmlCanvasSize(); + htmlElement.append(canvasElement); + } + + /// The root HTML element for this canvas. + /// + /// This element contains the canvas used to draw the UI. Unlike the canvas, + /// this element is permanent. It is never replaced or deleted, until this + /// canvas is disposed of via [dispose]. + /// + /// Conversely, the canvas that lives inside this element can be swapped, for + /// example, when the screen size changes, or when the WebGL context is lost + /// due to the browser tab becoming dormant. + final DomElement htmlElement = createDomElement('flt-canvas-container'); + + /// The underlying `` element used to display the pixels. + final DomCanvasElement canvasElement = createDomCanvasElement(); + int _pixelWidth = 0; + int _pixelHeight = 0; + + late final DomCanvasRenderingContextBitmapRenderer renderContext = + canvasElement.contextBitmapRenderer; + + double _currentDevicePixelRatio = -1; + + /// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device + /// pixels. + /// + /// The logical size of the canvas is not based on the size of the window + /// but on the size of the canvas, which, due to `ceil()` above, may not be + /// the same as the window. We do not round/floor/ceil the logical size as + /// CSS pixels can contain more than one physical pixel and therefore to + /// match the size of the window precisely we use the most precise floating + /// point value we can get. + void _updateLogicalHtmlCanvasSize() { + final double logicalWidth = _pixelWidth / window.devicePixelRatio; + final double logicalHeight = _pixelHeight / window.devicePixelRatio; + final DomCSSStyleDeclaration style = canvasElement.style; + style.width = '${logicalWidth}px'; + style.height = '${logicalHeight}px'; + _currentDevicePixelRatio = window.devicePixelRatio; + } + + /// Render the given [bitmap] with this [RenderCanvas]. + /// + /// The canvas will be resized to accomodate the bitmap immediately before + /// rendering it. + void render(DomImageBitmap bitmap) { + _ensureSize(ui.Size(bitmap.width.toDartDouble, bitmap.height.toDartDouble)); + renderContext.transferFromImageBitmap(bitmap); + } + + /// Ensures that this canvas can draw a frame of the given [size]. + void _ensureSize(ui.Size size) { + // Check if the frame is the same size as before, and if so, we don't need + // to resize the canvas. + if (size.width.ceil() == _pixelWidth && + size.height.ceil() == _pixelHeight) { + // The existing canvas doesn't need to be resized (unless the device pixel + // ratio changed). + if (window.devicePixelRatio != _currentDevicePixelRatio) { + _updateLogicalHtmlCanvasSize(); + } + return; + } + + // If the canvas is too large or too small, resize it to the exact size of + // the frame. We cannot allow the canvas to be larger than the screen + // because then when we call `transferFromImageBitmap()` the bitmap will + // be scaled to cover the entire canvas. + _pixelWidth = size.width.ceil(); + _pixelHeight = size.height.ceil(); + canvasElement.width = _pixelWidth.toDouble(); + canvasElement.height = _pixelHeight.toDouble(); + _updateLogicalHtmlCanvasSize(); + } + + void dispose() { + htmlElement.remove(); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart new file mode 100644 index 0000000000000..593390972377c --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart @@ -0,0 +1,142 @@ +// 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:meta/meta.dart'; + +import '../../engine.dart'; + +/// Caches canvases used to overlay platform views. +class RenderCanvasFactory { + RenderCanvasFactory() { + assert(() { + registerHotRestartListener(debugClear); + return true; + }()); + } + + /// The lazy-initialized singleton surface factory. + /// + /// [debugClear] causes this singleton to be reinitialized. + static RenderCanvasFactory get instance => + _instance ??= RenderCanvasFactory(); + + /// Returns the raw (potentially uninitialized) value of the singleton. + /// + /// Useful in tests for checking the lifecycle of this class. + static RenderCanvasFactory? get debugUninitializedInstance => _instance; + + // Override the current instance with a new one. + // + // This should only be used in tests. + static void debugSetInstance(RenderCanvasFactory newInstance) { + _instance = newInstance; + } + + static RenderCanvasFactory? _instance; + + /// The base canvas to paint on. This is the default canvas which will be + /// painted to. If there are no platform views, then this canvas will render + /// the entire scene. + final RenderCanvas baseCanvas = RenderCanvas(); + + /// A surface used specifically for `Picture.toImage` when software rendering + /// is supported. + late final Surface pictureToImageSurface = Surface(); + + /// Canvases created by this factory which are currently in use. + final List _liveCanvases = []; + + /// Canvases created by this factory which are no longer in use. These can be + /// reused. + final List _cache = []; + + /// The number of canvases which have been created by this factory. + int get _canvasCount => _liveCanvases.length + _cache.length + 1; + + /// The number of surfaces created by this factory. Used for testing. + @visibleForTesting + int get debugSurfaceCount => _canvasCount; + + /// Returns the number of cached surfaces. + /// + /// Useful in tests. + int get debugCacheSize => _cache.length; + + /// Gets an overlay canvas from the cache or creates a new one if there are + /// none in the cache. + RenderCanvas getCanvas() { + if (_cache.isNotEmpty) { + final RenderCanvas canvas = _cache.removeLast(); + _liveCanvases.add(canvas); + return canvas; + } else { + final RenderCanvas canvas = RenderCanvas(); + _liveCanvases.add(canvas); + return canvas; + } + } + + /// Releases all surfaces so they can be reused in the next frame. + /// + /// If a released surface is in the DOM, it is not removed. This allows the + /// engine to release the surfaces at the end of the frame so they are ready + /// to be used in the next frame, but still used for painting in the current + /// frame. + void releaseCanvases() { + _cache.addAll(_liveCanvases); + _liveCanvases.clear(); + } + + /// Removes all surfaces except the base surface from the DOM. + /// + /// This is called at the beginning of the frame to prepare for painting into + /// the new surfaces. + void removeSurfacesFromDom() { + _cache.forEach(_removeFromDom); + } + + // Removes [canvas] from the DOM. + void _removeFromDom(RenderCanvas canvas) { + canvas.htmlElement.remove(); + } + + /// Signals that a canvas is no longer being used. It can be reused. + void releaseCanvas(RenderCanvas canvas) { + assert(canvas != baseCanvas, 'Attempting to release the base canvas'); + assert( + _liveCanvases.contains(canvas), + 'Attempting to release a Canvas which ' + 'was not created by this factory'); + canvas.htmlElement.remove(); + _liveCanvases.remove(canvas); + _cache.add(canvas); + } + + /// Returns [true] if [canvas] is currently being used to paint content. + /// + /// The base canvas always counts as live. + /// + /// If a canvas is not live, then it must be in the cache and ready to be + /// reused. + bool isLive(RenderCanvas canvas) { + if (canvas == baseCanvas || _liveCanvases.contains(canvas)) { + return true; + } + assert(_cache.contains(canvas)); + return false; + } + + /// Dispose all canvases created by this factory. Used in tests. + void debugClear() { + for (final RenderCanvas canvas in _cache) { + canvas.dispose(); + } + for (final RenderCanvas canvas in _liveCanvases) { + canvas.dispose(); + } + baseCanvas.dispose(); + _liveCanvases.clear(); + _cache.clear(); + _instance = null; + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 76719d68dcea8..db2277c65ac6b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -11,11 +11,10 @@ import '../configuration.dart'; import '../dom.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; -import '../window.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; -import 'renderer.dart'; -import 'surface_factory.dart'; +import 'picture.dart'; +import 'render_canvas.dart'; import 'util.dart'; // Only supported in profile/release mode. Allows Flutter to use MSAA but @@ -26,8 +25,7 @@ typedef SubmitCallback = bool Function(SurfaceFrame, CkCanvas); /// A frame which contains a canvas to be drawn into. class SurfaceFrame { - SurfaceFrame(this.skiaSurface, this.submitCallback) - : _submitted = false; + SurfaceFrame(this.skiaSurface, this.submitCallback) : _submitted = false; final CkSurface skiaSurface; final SubmitCallback submitCallback; @@ -82,19 +80,16 @@ class Surface { int? _glContext; int? _skiaCacheBytes; - /// The root HTML element for this surface. - /// - /// This element contains the canvas used to draw the UI. Unlike the canvas, - /// this element is permanent. It is never replaced or deleted, until this - /// surface is disposed of via [dispose]. - /// - /// Conversely, the canvas that lives inside this element can be swapped, for - /// example, when the screen size changes, or when the WebGL context is lost - /// due to the browser tab becoming dormant. - final DomElement htmlElement = createDomElement('flt-canvas-container'); + /// The underlying OffscreenCanvas element used for this surface. + DomOffscreenCanvas? _offscreenCanvas; + + /// Returns the underlying OffscreenCanvas. Should only be used in tests. + DomOffscreenCanvas? get debugOffscreenCanvas => _offscreenCanvas; + + /// The backing this Surface in the case that OffscreenCanvas isn't + /// supported. + DomCanvasElement? _canvasElement; - /// The underlying `` element used for this surface. - DomCanvasElement? htmlCanvas; int _pixelWidth = -1; int _pixelHeight = -1; int _sampleCount = -1; @@ -112,7 +107,33 @@ class Surface { } } - bool _addedToScene = false; + Future rasterizeToCanvas( + ui.Size frameSize, RenderCanvas canvas, List pictures) async { + final CkCanvas skCanvas = _surface!.getCanvas(); + skCanvas.clear(const ui.Color(0x00000000)); + pictures.forEach(skCanvas.drawPicture); + _surface!.flush(); + + DomImageBitmap bitmap; + if (Surface.offscreenCanvasSupported) { + bitmap = (await createSizedOffscreenImageBitmap( + _offscreenCanvas!, + 0, + _pixelHeight - frameSize.height.toInt(), + frameSize.width.toInt(), + frameSize.height.toInt(), + ))!; + } else { + bitmap = (await createSizedImageBitmap( + _canvasElement!, + 0, + _pixelHeight - frameSize.height.toInt(), + frameSize.width.toInt(), + frameSize.height.toInt(), + ))!; + } + canvas.render(bitmap); + } /// Acquire a frame of the given [size] containing a drawable canvas. /// @@ -129,21 +150,16 @@ class Surface { return SurfaceFrame(surface, submitCallback); } - void addToScene() { - if (!_addedToScene) { - CanvasKitRenderer.instance.sceneHost!.prepend(htmlElement); - } - _addedToScene = true; - } - ui.Size? _currentCanvasPhysicalSize; ui.Size? _currentSurfaceSize; - double _currentDevicePixelRatio = -1; /// This is only valid after the first frame or if [ensureSurface] has been /// called - bool get usingSoftwareBackend => _glContext == null || - _grContext == null || webGLVersion == -1 || configuration.canvasKitForceCpuOnly; + bool get usingSoftwareBackend => + _glContext == null || + _grContext == null || + webGLVersion == -1 || + configuration.canvasKitForceCpuOnly; /// Ensure that the initial surface exists and has a size of at least [size]. /// @@ -159,22 +175,10 @@ class Surface { } // TODO(jonahwilliams): this is somewhat wasteful. We should probably // eagerly setup this surface instead of delaying until the first frame? - // Or at least cache the estimated window size. + // Or at least cache the estimated window sizeThis is the first frame we have rendered with this canvas. createOrUpdateSurface(size); } - /// This method is not supported if software rendering is used. - CkSurface createRenderTargetSurface(ui.Size size) { - assert(!usingSoftwareBackend); - - final SkSurface skSurface = canvasKit.MakeRenderTarget( - _grContext!, - size.width.ceil(), - size.height.ceil(), - )!; - return CkSurface(skSurface, _glContext); - } - /// Creates a and SkSurface for the given [size]. CkSurface createOrUpdateSurface(ui.Size size) { if (size.isEmpty) { @@ -188,11 +192,6 @@ class Surface { if (previousSurfaceSize != null && size.width == previousSurfaceSize.width && size.height == previousSurfaceSize.height) { - // The existing surface is still reusable. - if (window.devicePixelRatio != _currentDevicePixelRatio) { - _updateLogicalHtmlCanvasSize(); - _translateCanvas(); - } return _surface!; } @@ -205,12 +204,16 @@ class Surface { final ui.Size newSize = size * 1.4; _surface?.dispose(); _surface = null; - htmlCanvas!.width = newSize.width; - htmlCanvas!.height = newSize.height; + if (Surface.offscreenCanvasSupported) { + _offscreenCanvas!.width = newSize.width; + _offscreenCanvas!.height = newSize.height; + } else { + _canvasElement!.width = newSize.width; + _canvasElement!.height = newSize.height; + } _currentCanvasPhysicalSize = newSize; _pixelWidth = newSize.width.ceil(); _pixelHeight = newSize.height.ceil(); - _updateLogicalHtmlCanvasSize(); } } @@ -218,57 +221,20 @@ class Surface { if (_forceNewContext || _currentCanvasPhysicalSize == null) { _surface?.dispose(); _surface = null; - _addedToScene = false; _grContext?.releaseResourcesAndAbandonContext(); _grContext?.delete(); _grContext = null; _createNewCanvas(size); _currentCanvasPhysicalSize = size; - } else if (window.devicePixelRatio != _currentDevicePixelRatio) { - _updateLogicalHtmlCanvasSize(); } - _currentDevicePixelRatio = window.devicePixelRatio; _currentSurfaceSize = size; - _translateCanvas(); _surface?.dispose(); _surface = _createNewSurface(size); return _surface!; } - /// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device - /// pixels. - /// - /// The logical size of the canvas is not based on the size of the window - /// but on the size of the canvas, which, due to `ceil()` above, may not be - /// the same as the window. We do not round/floor/ceil the logical size as - /// CSS pixels can contain more than one physical pixel and therefore to - /// match the size of the window precisely we use the most precise floating - /// point value we can get. - void _updateLogicalHtmlCanvasSize() { - final double logicalWidth = _pixelWidth / window.devicePixelRatio; - final double logicalHeight = _pixelHeight / window.devicePixelRatio; - final DomCSSStyleDeclaration style = htmlCanvas!.style; - style.width = '${logicalWidth}px'; - style.height = '${logicalHeight}px'; - } - - /// Translate the canvas so the surface covers the visible portion of the - /// screen. - /// - /// The may be larger than the visible screen, but the SkSurface is - /// exactly the size of the visible screen. Unfortunately, the SkSurface is - /// drawn in the lower left corner of the , and without translation, - /// only the top left of the is visible. So we shift the canvas up so - /// the bottom left corner is visible. - void _translateCanvas() { - final int surfaceHeight = _currentSurfaceSize!.height.ceil(); - final double offset = - (_pixelHeight - surfaceHeight) / window.devicePixelRatio; - htmlCanvas!.style.transform = 'translate(0, -${offset}px)'; - } - JSVoid _contextRestoredListener(DomEvent event) { assert( _contextLost, @@ -282,16 +248,11 @@ class Surface { } JSVoid _contextLostListener(DomEvent event) { - assert(event.target == htmlCanvas, + assert(event.target == _offscreenCanvas || event.target == _canvasElement, 'Received a context lost event for a disposed canvas'); - final SurfaceFactory factory = SurfaceFactory.instance; _contextLost = true; - if (factory.isLive(this)) { - _forceNewContext = true; - event.preventDefault(); - } else { - dispose(); - } + _forceNewContext = true; + event.preventDefault(); } /// This function is expensive. @@ -299,18 +260,32 @@ class Surface { /// It's better to reuse canvas if possible. void _createNewCanvas(ui.Size physicalSize) { // Clear the container, if it's not empty. We're going to create a new . - if (this.htmlCanvas != null) { - this.htmlCanvas!.removeEventListener( - 'webglcontextrestored', - _cachedContextRestoredListener, - false, - ); - this.htmlCanvas!.removeEventListener( - 'webglcontextlost', - _cachedContextLostListener, - false, - ); - this.htmlCanvas!.remove(); + if (_offscreenCanvas != null) { + _offscreenCanvas!.removeEventListener( + 'webglcontextrestored', + _cachedContextRestoredListener, + false, + ); + _offscreenCanvas!.removeEventListener( + 'webglcontextlost', + _cachedContextLostListener, + false, + ); + _offscreenCanvas = null; + _cachedContextRestoredListener = null; + _cachedContextLostListener = null; + } else if (_canvasElement != null) { + _canvasElement!.removeEventListener( + 'webglcontextrestored', + _cachedContextRestoredListener, + false, + ); + _canvasElement!.removeEventListener( + 'webglcontextlost', + _cachedContextLostListener, + false, + ); + _canvasElement = null; _cachedContextRestoredListener = null; _cachedContextLostListener = null; } @@ -319,25 +294,22 @@ class Surface { // we ensure that the rendred picture covers the entire browser window. _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); - final DomCanvasElement htmlCanvas = createDomCanvasElement( - width: _pixelWidth, - height: _pixelHeight, - ); - this.htmlCanvas = htmlCanvas; - - // The DOM elements used to render pictures are used purely to put pixels on - // the screen. They have no semantic information. If an assistive technology - // attempts to scan picture content it will look like garbage and confuse - // users. UI semantics are exported as a separate DOM tree rendered parallel - // to pictures. - // - // Why are layer and scene elements not hidden from ARIA? Because those - // elements may contain platform views, and platform views must be - // accessible. - htmlCanvas.setAttribute('aria-hidden', 'true'); - - htmlCanvas.style.position = 'absolute'; - _updateLogicalHtmlCanvasSize(); + DomEventTarget htmlCanvas; + if (Surface.offscreenCanvasSupported) { + final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas( + _pixelWidth, + _pixelHeight, + ); + htmlCanvas = offscreenCanvas; + _offscreenCanvas = offscreenCanvas; + _canvasElement = null; + } else { + final DomCanvasElement canvas = + createDomCanvasElement(width: _pixelWidth, height: _pixelHeight); + htmlCanvas = canvas; + _canvasElement = canvas; + _offscreenCanvas = null; + } // When the browser tab using WebGL goes dormant the browser and/or OS may // decide to clear GPU resources to let other tabs/programs use the GPU. @@ -345,7 +317,8 @@ class Surface { // notification. When we receive this notification we force a new context. // // See also: https://www.khronos.org/webgl/wiki/HandlingContextLost - _cachedContextRestoredListener = createDomEventListener(_contextRestoredListener); + _cachedContextRestoredListener = + createDomEventListener(_contextRestoredListener); _cachedContextLostListener = createDomEventListener(_contextLostListener); htmlCanvas.addEventListener( 'webglcontextlost', @@ -361,15 +334,24 @@ class Surface { _contextLost = false; if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) { - final int glContext = canvasKit.GetWebGLContext( - htmlCanvas, - SkWebGLContextOptions( - // Default to no anti-aliasing. Paint commands can be explicitly - // anti-aliased by setting their `Paint` object's `antialias` property. - antialias: _kUsingMSAA ? 1 : 0, - majorVersion: webGLVersion.toDouble(), - ), - ).toInt(); + int glContext = 0; + final SkWebGLContextOptions options = SkWebGLContextOptions( + // Default to no anti-aliasing. Paint commands can be explicitly + // anti-aliased by setting their `Paint` object's `antialias` property. + antialias: _kUsingMSAA ? 1 : 0, + majorVersion: webGLVersion.toDouble(), + ); + if (Surface.offscreenCanvasSupported) { + glContext = canvasKit.GetOffscreenWebGLContext( + _offscreenCanvas!, + options, + ).toInt(); + } else { + glContext = canvasKit.GetWebGLContext( + _canvasElement!, + options, + ).toInt(); + } _glContext = glContext; @@ -387,40 +369,38 @@ class Surface { _syncCacheBytes(); } } - - htmlElement.append(htmlCanvas); } void _initWebglParams() { - final WebGLContext gl = htmlCanvas!.getGlContext(webGLVersion); + WebGLContext gl; + if (Surface.offscreenCanvasSupported) { + gl = _offscreenCanvas!.getGlContext(webGLVersion); + } else { + gl = _canvasElement!.getGlContext(webGLVersion); + } _sampleCount = gl.getParameter(gl.samples); _stencilBits = gl.getParameter(gl.stencilBits); } CkSurface _createNewSurface(ui.Size size) { - assert(htmlCanvas != null); + assert(_offscreenCanvas != null || _canvasElement != null); if (webGLVersion == -1) { - return _makeSoftwareCanvasSurface( - htmlCanvas!, 'WebGL support not detected'); + return _makeSoftwareCanvasSurface('WebGL support not detected'); } else if (configuration.canvasKitForceCpuOnly) { - return _makeSoftwareCanvasSurface( - htmlCanvas!, 'CPU rendering forced by application'); + return _makeSoftwareCanvasSurface('CPU rendering forced by application'); } else if (_glContext == 0) { - return _makeSoftwareCanvasSurface( - htmlCanvas!, 'Failed to initialize WebGL context'); + return _makeSoftwareCanvasSurface('Failed to initialize WebGL context'); } else { final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( - _grContext!, - size.width.roundToDouble(), - size.height.roundToDouble(), - SkColorSpaceSRGB, - _sampleCount, - _stencilBits - ); + _grContext!, + size.width.roundToDouble(), + size.height.roundToDouble(), + SkColorSpaceSRGB, + _sampleCount, + _stencilBits); if (skSurface == null) { - return _makeSoftwareCanvasSurface( - htmlCanvas!, 'Failed to initialize WebGL surface'); + return _makeSoftwareCanvasSurface('Failed to initialize WebGL surface'); } return CkSurface(skSurface, _glContext); @@ -429,14 +409,20 @@ class Surface { static bool _didWarnAboutWebGlInitializationFailure = false; - CkSurface _makeSoftwareCanvasSurface( - DomCanvasElement htmlCanvas, String reason) { + CkSurface _makeSoftwareCanvasSurface(String reason) { if (!_didWarnAboutWebGlInitializationFailure) { printWarning('WARNING: Falling back to CPU-only rendering. $reason.'); _didWarnAboutWebGlInitializationFailure = true; } + + SkSurface surface; + if (Surface.offscreenCanvasSupported) { + surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!); + } else { + surface = canvasKit.MakeSWCanvasSurface(_canvasElement!); + } return CkSurface( - canvasKit.MakeSWCanvasSurface(htmlCanvas), + surface, null, ); } @@ -447,15 +433,19 @@ class Surface { } void dispose() { - htmlCanvas?.removeEventListener( + _offscreenCanvas?.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false); - htmlCanvas?.removeEventListener( + _offscreenCanvas?.removeEventListener( 'webglcontextrestored', _cachedContextRestoredListener, false); _cachedContextLostListener = null; _cachedContextRestoredListener = null; - htmlElement.remove(); _surface?.dispose(); } + + /// Safari 15 doesn't support OffscreenCanvas at all. Safari 16 supports + /// OffscreenCanvas, but only with the context2d API, not WebGL. + static bool get offscreenCanvasSupported => + browserSupportsOffscreenCanvas && !isSafari; } /// A Dart wrapper around Skia's CkSurface. diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart deleted file mode 100644 index ee5b001dd8e04..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart +++ /dev/null @@ -1,167 +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 show max; - -import 'package:meta/meta.dart'; - -import '../../engine.dart'; - -/// Caches surfaces used to overlay platform views. -class SurfaceFactory { - SurfaceFactory(int maximumSurfaces) - : maximumSurfaces = math.max(maximumSurfaces, 1) { - assert(() { - if (maximumSurfaces < 1) { - printWarning('Attempted to create a $SurfaceFactory with ' - '$maximumSurfaces maximum surfaces. At least 1 surface is required ' - 'for rendering.'); - } - registerHotRestartListener(debugClear); - return true; - }()); - } - - /// The lazy-initialized singleton surface factory. - /// - /// [debugClear] causes this singleton to be reinitialized. - static SurfaceFactory get instance => - _instance ??= SurfaceFactory(configuration.canvasKitMaximumSurfaces); - - /// Returns the raw (potentially uninitialized) value of the singleton. - /// - /// Useful in tests for checking the lifecycle of this class. - static SurfaceFactory? get debugUninitializedInstance => _instance; - - // Override the current instance with a new one. - // - // This should only be used in tests. - static void debugSetInstance(SurfaceFactory newInstance) { - _instance = newInstance; - } - - static SurfaceFactory? _instance; - - /// The base surface to paint on. This is the default surface which will be - /// painted to. If there are no platform views, then this surface will receive - /// all painting commands. - final Surface baseSurface = Surface(); - - /// The maximum number of surfaces which can be live at once. - final int maximumSurfaces; - - /// A surface used specifically for `Picture.toImage` when software rendering - /// is supported. - late final Surface pictureToImageSurface = Surface(); - - /// The maximum number of assignable overlays. - /// - /// This is just `maximumSurfaces - 1` (the maximum number of surfaces minus - /// the required base surface). - int get maximumOverlays => maximumSurfaces - 1; - - /// Surfaces created by this factory which are currently in use. - final List _liveSurfaces = []; - - /// Surfaces created by this factory which are no longer in use. These can be - /// reused. - final List _cache = []; - - /// The number of surfaces which have been created by this factory. - int get _surfaceCount => _liveSurfaces.length + _cache.length + 1; - - /// The number of available overlay surfaces. - /// - /// This does not include the base surface. - int get numAvailableOverlays => maximumOverlays - _liveSurfaces.length; - - /// The number of surfaces created by this factory. Used for testing. - @visibleForTesting - int get debugSurfaceCount => _surfaceCount; - - /// Returns the number of cached surfaces. - /// - /// Useful in tests. - int get debugCacheSize => _cache.length; - - /// Gets an overlay surface from the cache or creates a new one if it wouldn't - /// exceed the maximum. If there are no available surfaces, returns `null`. - Surface? getSurface() { - if (_cache.isNotEmpty) { - final Surface surface = _cache.removeLast(); - _liveSurfaces.add(surface); - return surface; - } else if (debugSurfaceCount < maximumSurfaces) { - final Surface surface = Surface(); - _liveSurfaces.add(surface); - return surface; - } else { - return null; - } - } - - /// Releases all surfaces so they can be reused in the next frame. - /// - /// If a released surface is in the DOM, it is not removed. This allows the - /// engine to release the surfaces at the end of the frame so they are ready - /// to be used in the next frame, but still used for painting in the current - /// frame. - void releaseSurfaces() { - _cache.addAll(_liveSurfaces); - _liveSurfaces.clear(); - } - - /// Removes all surfaces except the base surface from the DOM. - /// - /// This is called at the beginning of the frame to prepare for painting into - /// the new surfaces. - void removeSurfacesFromDom() { - _cache.forEach(_removeFromDom); - } - - // Removes [surface] from the DOM. - void _removeFromDom(Surface surface) { - surface.htmlElement.remove(); - } - - /// Signals that a surface is no longer being used. It can be reused. - void releaseSurface(Surface surface) { - assert(surface != baseSurface, 'Attempting to release the base surface'); - assert( - _liveSurfaces.contains(surface), - 'Attempting to release a Surface which ' - 'was not created by this factory'); - surface.htmlElement.remove(); - _liveSurfaces.remove(surface); - _cache.add(surface); - } - - /// Returns [true] if [surface] is currently being used to paint content. - /// - /// The base surface always counts as live. - /// - /// If a surface is not live, then it must be in the cache and ready to be - /// reused. - bool isLive(Surface surface) { - if (surface == baseSurface || - _liveSurfaces.contains(surface)) { - return true; - } - assert(_cache.contains(surface)); - return false; - } - - /// Dispose all surfaces created by this factory. Used in tests. - void debugClear() { - for (final Surface surface in _cache) { - surface.dispose(); - } - for (final Surface surface in _liveSurfaces) { - surface.dispose(); - } - baseSurface.dispose(); - _liveSurfaces.clear(); - _cache.clear(); - _instance = null; - } -} diff --git a/lib/web_ui/lib/src/engine/configuration.dart b/lib/web_ui/lib/src/engine/configuration.dart index 151e2c2bc83d7..1a5dff3bdd696 100644 --- a/lib/web_ui/lib/src/engine/configuration.dart +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -257,15 +257,10 @@ class FlutterConfiguration { 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', ); - /// The maximum number of overlay surfaces that the CanvasKit renderer will use. - /// - /// Overlay surfaces are extra WebGL `` elements used to paint on top - /// of platform views. Too many platform views can cause the browser to run - /// out of resources (memory, CPU, GPU) to handle the content efficiently. - /// The number of overlay surfaces is therefore limited. - /// - /// This value can be specified using either the `FLUTTER_WEB_MAXIMUM_SURFACES` - /// environment variable, or using the runtime configuration. + /// This is deprecated. The CanvasKit renderer will only ever create one + /// WebGL context, obviating the problem this configuration was meant to + /// solve originally. + @Deprecated('Setting canvasKitMaximumSurfaces has no effect') int get canvasKitMaximumSurfaces => _configuration?.canvasKitMaximumSurfaces?.toInt() ?? _defaultCanvasKitMaximumSurfaces; static const int _defaultCanvasKitMaximumSurfaces = int.fromEnvironment( diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 9eab879b37c63..23e055afc2bef 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -198,6 +198,36 @@ external DomIntl get domIntl; @JS('Symbol') external DomSymbol get domSymbol; +@JS('createImageBitmap') +external JSPromise _createImageBitmap(JSAny source); +Future createImageBitmap(JSAny source) => + js_util.promiseToFuture(_createImageBitmap(source)); + +@JS('createImageBitmap') +external JSPromise _createSizedImageBitmap(DomCanvasElement canvas, JSNumber sx, + JSNumber sy, JSNumber sw, JSNumber sh); +Future createSizedImageBitmap( + DomCanvasElement canvas, int sx, int sy, int sw, int sh) => + js_util.promiseToFuture( + _createSizedImageBitmap(canvas, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); + +@JS('createImageBitmap') +external JSPromise _createSizedImageBitmapFromImageData( + DomImageData imageData, JSNumber sx, JSNumber sy, JSNumber sw, JSNumber sh); +Future createSizedImageBitmapFromImageData( + DomImageData imageData, int sx, int sy, int sw, int sh) => + js_util.promiseToFuture( + _createSizedImageBitmapFromImageData( + imageData, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); + +@JS('createImageBitmap') +external JSPromise _createSizedOffscreenImageBitmap(DomOffscreenCanvas canvas, + JSNumber sx, JSNumber sy, JSNumber sw, JSNumber sh); +Future createSizedOffscreenImageBitmap( + DomOffscreenCanvas canvas, int sx, int sy, int sw, int sh) => + js_util.promiseToFuture(_createSizedOffscreenImageBitmap( + canvas, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); + @JS() @staticInterop class DomNavigator {} @@ -1407,7 +1437,7 @@ extension DomCanvasRenderingContextWebGlExtension class DomCanvasRenderingContextBitmapRenderer {} extension DomCanvasRenderingContextBitmapRendererExtension - on DomCanvasRenderingContextBitmapRenderer { + on DomCanvasRenderingContextBitmapRenderer { external void transferFromImageBitmap(DomImageBitmap bitmap); } @@ -1415,10 +1445,13 @@ extension DomCanvasRenderingContextBitmapRendererExtension @staticInterop class DomImageData { external factory DomImageData._(JSAny? data, JSNumber sw, JSNumber sh); + external factory DomImageData._empty(JSNumber sw, JSNumber sh); } -DomImageData createDomImageData(Object? data, int sw, int sh) => - DomImageData._(data?.toJSAnyShallow, sw.toJS, sh.toJS); +DomImageData createDomImageData(Object data, int sw, int sh) => + DomImageData._(data.toJSAnyShallow, sw.toJS, sh.toJS); +DomImageData createBlankDomImageData(int sw, int sh) => + DomImageData._empty(sw.toJS, sh.toJS); extension DomImageDataExtension on DomImageData { @JS('data') @@ -1436,33 +1469,6 @@ extension DomImageBitmapExtension on DomImageBitmap { external void close(); } - -@JS('createImageBitmap') -external JSPromise _createImageBitmap1( - JSAny source, -); -@JS('createImageBitmap') -external JSPromise _createImageBitmap2( - JSAny source, - JSNumber x, - JSNumber y, - JSNumber width, - JSNumber height, -); -JSPromise createImageBitmap(JSAny source, [({int x, int y, int width, int height})? bounds]) { - if (bounds != null) { - return _createImageBitmap2( - source, - bounds.x.toJS, - bounds.y.toJS, - bounds.width.toJS, - bounds.height.toJS - ); - } else { - return _createImageBitmap1(source); - } -} - @JS() @staticInterop class DomCanvasPattern {} @@ -1505,7 +1511,8 @@ MockHttpFetchResponseFactory? mockHttpFetchResponseFactory; /// [httpFetchText] instead. Future httpFetch(String url) async { if (mockHttpFetchResponseFactory != null) { - final MockHttpFetchResponse? response = await mockHttpFetchResponseFactory!(url); + final MockHttpFetchResponse? response = + await mockHttpFetchResponseFactory!(url); if (response != null) { return response; } @@ -1762,8 +1769,7 @@ class MockHttpFetchPayload implements HttpFetchPayload { while (currentIndex < totalLength) { final int chunkSize = math.min(_chunkSize, totalLength - currentIndex); final Uint8List chunk = Uint8List.sublistView( - _byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize - ); + _byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize); callback(chunk.toJS as T); currentIndex += chunkSize; } @@ -1773,10 +1779,12 @@ class MockHttpFetchPayload implements HttpFetchPayload { Future asByteBuffer() async => _byteBuffer; @override - Future json() async => throw AssertionError('json not supported by mock'); + Future json() async => + throw AssertionError('json not supported by mock'); @override - Future text() async => throw AssertionError('text not supported by mock'); + Future text() async => + throw AssertionError('text not supported by mock'); } /// Indicates a missing HTTP payload when one was expected, such as when @@ -2311,9 +2319,7 @@ DomBlob createDomBlob(List parts, [Map? options]) { return DomBlob(parts.toJSAnyShallow as JSArray); } else { return DomBlob.withOptions( - parts.toJSAnyShallow as JSArray, - options.toJSAnyDeep - ); + parts.toJSAnyShallow as JSArray, options.toJSAnyDeep); } } @@ -2845,6 +2851,13 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas { } } + WebGLContext getGlContext(int majorVersion) { + if (majorVersion == 1) { + return getContext('webgl')! as WebGLContext; + } + return getContext('webgl2')! as WebGLContext; + } + @JS('convertToBlob') external JSPromise _convertToBlob1(); @JS('convertToBlob') @@ -2858,6 +2871,11 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas { } return js_util.promiseToFuture(blob); } + + @JS('transferToImageBitmap') + external JSAny? _transferToImageBitmap(); + DomImageBitmap transferToImageBitmap() => + _transferToImageBitmap()! as DomImageBitmap; } DomOffscreenCanvas createDomOffscreenCanvas(int width, int height) => @@ -3451,8 +3469,8 @@ class DomSegments {} extension DomSegmentsExtension on DomSegments { DomIteratorWrapper iterator() { - final DomIterator segmentIterator = - js_util.callMethod(this, domSymbol.iterator, const []) as DomIterator; + final DomIterator segmentIterator = js_util + .callMethod(this, domSymbol.iterator, const []) as DomIterator; return DomIteratorWrapper(segmentIterator); } } @@ -3589,10 +3607,8 @@ external JSAny? get _finalizationRegistryConstructor; // dart2js that causes a crash in the Google3 build if we do use a factory // constructor. See b/284478971 DomFinalizationRegistry createDomFinalizationRegistry(JSFunction cleanup) => - js_util.callConstructor( - _finalizationRegistryConstructor!.toObjectShallow, - [cleanup] - ); + js_util.callConstructor( + _finalizationRegistryConstructor!.toObjectShallow, [cleanup]); extension DomFinalizationRegistryExtension on DomFinalizationRegistry { @JS('register') @@ -3601,11 +3617,12 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry { @JS('register') external JSVoid _register2(JSAny target, JSAny value, JSAny token); void register(Object target, Object value, [Object? token]) { - if (token != null) { - _register2(target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow); - } else { - _register1(target.toJSAnyShallow, value.toJSAnyShallow); - } + if (token != null) { + _register2( + target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow); + } else { + _register1(target.toJSAnyShallow, value.toJSAnyShallow); + } } @JS('unregister') @@ -3617,6 +3634,11 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry { bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; +@JS('window.OffscreenCanvas') +external JSAny? get _offscreenCanvasConstructor; + +bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null; + @JS() @staticInterop extension JSArrayExtension on JSArray { diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 361e823b6e37e..46a436db87726 100644 --- a/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -163,7 +163,7 @@ void testMain() { // Regression test for https://github.com/flutter/flutter/issues/121758 test('resources used in temporary surfaces for Image.toByteData can cross to rendering overlays', () async { final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - SurfaceFactory.instance.debugClear(); + RenderCanvasFactory.instance.debugClear(); ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index a8a2b04ce9469..bf0fd59602968 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -24,7 +24,7 @@ void setUpCanvasKitTest() { tearDown(() { HtmlViewEmbedder.instance.debugClear(); - SurfaceFactory.instance.debugClear(); + RenderCanvasFactory.instance.debugClear(); }); setUp(() => diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index 5efacf30b59a1..40b5060fb5f53 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -42,10 +41,10 @@ void testMain() { // The platform view is now split in two parts. The contents live // as a child of the glassPane, and the slot lives in the glassPane // shadow root. The slot is the one that has pointer events auto. - final DomElement contents = flutterViewEmbedder.glassPaneElement - .querySelector('#view-0')!; - final DomElement slot = flutterViewEmbedder.sceneElement! - .querySelector('slot')!; + final DomElement contents = + flutterViewEmbedder.glassPaneElement.querySelector('#view-0')!; + final DomElement slot = + flutterViewEmbedder.sceneElement!.querySelector('slot')!; final DomElement contentsHost = contents.parent!; final DomElement slotHost = slot.parent!; @@ -292,8 +291,7 @@ void testMain() { }); test('renders overlays on top of platform views', () async { - expect(SurfaceFactory.instance.debugCacheSize, 0); - expect(configuration.canvasKitMaximumSurfaces, 8); + expect(RenderCanvasFactory.instance.debugCacheSize, 0); final CkPicture testPicture = paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint()); @@ -339,8 +337,8 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, + _overlay, ]); // Frame 2: @@ -372,7 +370,7 @@ void testMain() { ]); // Frame 4: - // Render: more platform views than max cache size. + // Render: more platform views than max overlay count. // Expect: main canvas, backup overlay, maximum overlays. await Future.delayed(Duration.zero); renderTestScene(viewCount: 16); @@ -391,7 +389,6 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _platformView, _platformView, @@ -401,6 +398,7 @@ void testMain() { _platformView, _platformView, _platformView, + _overlay, ]); // Frame 5: @@ -477,7 +475,6 @@ void testMain() { // Render: Views 1-10 // Expect: main canvas plus platform view overlays; empty cache. renderTestScene([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - expect(SurfaceFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -493,10 +490,10 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _platformView, _platformView, + _overlay, ]); // Frame 2: @@ -504,7 +501,6 @@ void testMain() { // Expect: main canvas plus platform view overlays; empty cache. await Future.delayed(Duration.zero); renderTestScene([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(SurfaceFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -520,10 +516,10 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _platformView, _platformView, + _overlay, ]); // Frame 3: @@ -546,10 +542,10 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _platformView, _platformView, + _overlay, ]); // Frame 4: @@ -572,10 +568,10 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _platformView, _platformView, + _overlay, ]); // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 @@ -599,8 +595,7 @@ void testMain() { ]); expect( - flutterViewEmbedder.glassPaneElement - .querySelector('flt-platform-view'), + flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'), isNotNull, ); @@ -615,13 +610,14 @@ void testMain() { ]); expect( - flutterViewEmbedder.glassPaneElement - .querySelector('flt-platform-view'), + flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'), isNull, ); }); - test('does not crash when resizing the window after textures have been registered', () async { + test( + 'does not crash when resizing the window after textures have been registered', + () async { ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -664,7 +660,7 @@ void testMain() { window.debugPhysicalSizeOverride = null; window.debugForceResize(); - // ImageDecoder is not supported in Safari or Firefox. + // ImageDecoder is not supported in Safari or Firefox. }, skip: isSafari || isFirefox); test('removed the DOM node of an unrendered platform view', () async { @@ -686,8 +682,7 @@ void testMain() { ]); expect( - flutterViewEmbedder.glassPaneElement - .querySelector('flt-platform-view'), + flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'), isNotNull, ); @@ -744,8 +739,8 @@ void testMain() { rasterizer.draw(sb.build().layerTree); } - final DomNode skPathDefs = flutterViewEmbedder.sceneElement! - .querySelector('#sk_path_defs')!; + final DomNode skPathDefs = + flutterViewEmbedder.sceneElement!.querySelector('#sk_path_defs')!; expect(skPathDefs.childNodes, hasLength(0)); @@ -782,121 +777,6 @@ void testMain() { ]); }); - test('does not crash when overlays are disabled', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - HtmlViewEmbedder.debugDisableOverlays = true; - ui_web.platformViewRegistry.registerViewFactory( - 'test-platform-view', - (int viewId) => createDomHTMLDivElement()..id = 'view-0', - ); - await createPlatformView(0, 'test-platform-view'); - - final LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(0, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - ]); - HtmlViewEmbedder.debugDisableOverlays = false; - }); - - test('works correctly with max overlays == 2', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - debugOverrideJsConfiguration( - { - 'canvasKitMaximumSurfaces': 2, - }.jsify() as JsFlutterConfiguration? - ); - expect(configuration.canvasKitMaximumSurfaces, 2); - expect(configuration.canvasKitVariant, isNot(CanvasKitVariant.auto)); - - SurfaceFactory.instance.debugClear(); - - expect(SurfaceFactory.instance.maximumSurfaces, 2); - expect(SurfaceFactory.instance.maximumOverlays, 1); - - ui_web.platformViewRegistry.registerViewFactory( - 'test-platform-view', - (int viewId) => createDomHTMLDivElement()..id = 'view-0', - ); - await createPlatformView(0, 'test-platform-view'); - await createPlatformView(1, 'test-platform-view'); - - LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(0, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - _overlay, - ]); - - sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(1, width: 10, height: 10); - sb.addPlatformView(0, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - _overlay, - _platformView, - ]); - - // Reset configuration - debugOverrideJsConfiguration(null); - }); - - test( - 'correctly renders when overlays are disabled and a subset ' - 'of views is used', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - HtmlViewEmbedder.debugDisableOverlays = true; - ui_web.platformViewRegistry.registerViewFactory( - 'test-platform-view', - (int viewId) => createDomHTMLDivElement()..id = 'view-0', - ); - await createPlatformView(0, 'test-platform-view'); - await createPlatformView(1, 'test-platform-view'); - - LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(0, width: 10, height: 10); - sb.addPlatformView(1, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - _platformView, - ]); - - sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(1, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - ]); - - HtmlViewEmbedder.debugDisableOverlays = false; - }); - test('does not create overlays for invisible platform views', () async { final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( @@ -957,7 +837,9 @@ void testMain() { _overlay, _platformView, _overlay, - ], reason: 'Overlays created after each group containing a visible view.'); + ], + reason: + 'Overlays created after each group containing a visible view.'); sb = LayerSceneBuilder(); sb.pushOffset(0, 0); @@ -1059,7 +941,9 @@ void testMain() { _platformView, _platformView, _platformView, - ], reason: 'Many invisible views can be rendered on top of the base overlay.'); + ], + reason: + 'Many invisible views can be rendered on top of the base overlay.'); sb = LayerSceneBuilder(); sb.pushOffset(0, 0); @@ -1108,19 +992,22 @@ enum _EmbeddedViewMarker { _EmbeddedViewMarker get _overlay => _EmbeddedViewMarker.overlay; _EmbeddedViewMarker get _platformView => _EmbeddedViewMarker.platformView; -const Map _tagToViewMarker = { +const Map _tagToViewMarker = + { 'flt-canvas-container': _EmbeddedViewMarker.overlay, 'flt-platform-view-slot': _EmbeddedViewMarker.platformView, }; -void _expectSceneMatches(List<_EmbeddedViewMarker> expectedMarkers, { +void _expectSceneMatches( + List<_EmbeddedViewMarker> expectedMarkers, { String? reason, }) { // Convert the scene elements to its corresponding array of _EmbeddedViewMarker final List<_EmbeddedViewMarker> sceneElements = flutterViewEmbedder .sceneElement!.children .where((DomElement element) => element.tagName != 'svg') - .map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!) + .map((DomElement element) => + _tagToViewMarker[element.tagName.toLowerCase()]!) .toList(); expect(sceneElements, expectedMarkers, reason: reason); diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart new file mode 100644 index 0000000000000..70aa4e6073ffa --- /dev/null +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -0,0 +1,95 @@ +// 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 'common.dart'; + +const MethodCodec codec = StandardMethodCodec(); + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('$RenderCanvasFactory', () { + setUpCanvasKitTest(); + + test('getCanvas', () { + final RenderCanvasFactory factory = RenderCanvasFactory(); + expect(factory.baseCanvas, isNotNull); + + expect(factory.debugSurfaceCount, equals(1)); + + // Get a canvas from the factory, it should be unique. + final RenderCanvas newCanvas = factory.getCanvas(); + expect(newCanvas, isNot(equals(factory.baseCanvas))); + + expect(factory.debugSurfaceCount, equals(2)); + + // Get another canvas from the factory. Now we are at maximum capacity. + final RenderCanvas anotherCanvas = factory.getCanvas(); + expect(anotherCanvas, isNot(equals(factory.baseCanvas))); + + expect(factory.debugSurfaceCount, equals(3)); + }); + + test('releaseCanvas', () { + final RenderCanvasFactory factory = RenderCanvasFactory(); + + // Create a new canvas and immediately release it. + final RenderCanvas canvas = factory.getCanvas(); + factory.releaseCanvas(canvas); + + // If we create a new canvas, it should be the same as the one we + // just created. + final RenderCanvas newCanvas = factory.getCanvas(); + expect(newCanvas, equals(canvas)); + }); + + test('isLive', () { + final RenderCanvasFactory factory = RenderCanvasFactory(); + + expect(factory.isLive(factory.baseCanvas), isTrue); + + final RenderCanvas canvas = factory.getCanvas(); + expect(factory.isLive(canvas), isTrue); + + factory.releaseCanvas(canvas); + expect(factory.isLive(canvas), isFalse); + }); + + test('hot restart', () { + void expectDisposed(RenderCanvas canvas) { + expect(canvas.canvasElement.isConnected, isFalse); + } + + final RenderCanvasFactory originalFactory = RenderCanvasFactory.instance; + expect(RenderCanvasFactory.debugUninitializedInstance, isNotNull); + + // Cause the surface and its canvas to be attached to the page + CanvasKitRenderer.instance.sceneHost! + .prepend(originalFactory.baseCanvas.htmlElement); + expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue); + + // Create a few overlay canvases + final List overlays = []; + for (int i = 0; i < 3; i++) { + final RenderCanvas canvas = originalFactory.getCanvas(); + CanvasKitRenderer.instance.sceneHost!.prepend(canvas.htmlElement); + overlays.add(canvas); + } + expect(originalFactory.debugSurfaceCount, 4); + + // Trigger hot restart clean-up logic and check that we indeed clean up. + debugEmulateHotRestart(); + expect(RenderCanvasFactory.debugUninitializedInstance, isNull); + expectDisposed(originalFactory.baseCanvas); + overlays.forEach(expectDisposed); + expect(originalFactory.debugSurfaceCount, 1); + }); + }); +} diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart new file mode 100644 index 0000000000000..75a0cfba2897c --- /dev/null +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -0,0 +1,62 @@ +// 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 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit', () { + setUpCanvasKitTest(); + setUp(() async { + window.debugOverrideDevicePixelRatio(1.0); + }); + + Future newBitmap(int width, int height) async { + return (await createSizedImageBitmapFromImageData( + createBlankDomImageData(width, height), + 0, + 0, + width, + height, + ))!; + } + + // Regression test for https://github.com/flutter/flutter/issues/75286 + test('updates canvas logical size when device-pixel ratio changes', + () async { + final RenderCanvas canvas = RenderCanvas(); + canvas.render(await newBitmap(10, 16)); + + expect(canvas.canvasElement.width, 10); + expect(canvas.canvasElement.height, 16); + expect(canvas.canvasElement.style.width, '10px'); + expect(canvas.canvasElement.style.height, '16px'); + + // Increase device-pixel ratio: this makes CSS pixels bigger, so we need + // fewer of them to cover the browser window. + window.debugOverrideDevicePixelRatio(2.0); + canvas.render(await newBitmap(10, 16)); + expect(canvas.canvasElement.width, 10); + expect(canvas.canvasElement.height, 16); + expect(canvas.canvasElement.style.width, '5px'); + expect(canvas.canvasElement.style.height, '8px'); + + // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need + // more of them to cover the browser window. + window.debugOverrideDevicePixelRatio(0.5); + canvas.render(await newBitmap(10, 16)); + expect(canvas.canvasElement.width, 10); + expect(canvas.canvasElement.height, 16); + expect(canvas.canvasElement.style.width, '20px'); + expect(canvas.canvasElement.style.height, '32px'); + }); + }); +} diff --git a/lib/web_ui/test/canvaskit/surface_factory_test.dart b/lib/web_ui/test/canvaskit/surface_factory_test.dart deleted file mode 100644 index 05db21472386c..0000000000000 --- a/lib/web_ui/test/canvaskit/surface_factory_test.dart +++ /dev/null @@ -1,103 +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 'common.dart'; - -const MethodCodec codec = StandardMethodCodec(); - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - group('$SurfaceFactory', () { - setUpCanvasKitTest(); - - test('cannot be created with size less than 1', () { - expect(SurfaceFactory(-1).maximumSurfaces, 1); - expect(SurfaceFactory(0).maximumSurfaces, 1); - expect(SurfaceFactory(1).maximumSurfaces, 1); - expect(SurfaceFactory(2).maximumSurfaces, 2); - }); - - test('getSurface', () { - final SurfaceFactory factory = SurfaceFactory(3); - expect(factory.baseSurface, isNotNull); - - expect(factory.debugSurfaceCount, equals(1)); - - // Get a surface from the factory, it should be unique. - final Surface? newSurface = factory.getSurface(); - expect(newSurface, isNot(equals(factory.baseSurface))); - - expect(factory.debugSurfaceCount, equals(2)); - - // Get another surface from the factory. Now we are at maximum capacity. - final Surface? anotherSurface = factory.getSurface(); - expect(anotherSurface, isNot(equals(factory.baseSurface))); - - expect(factory.debugSurfaceCount, equals(3)); - }); - - test('releaseSurface', () { - final SurfaceFactory factory = SurfaceFactory(3); - - // Create a new surface and immediately release it. - final Surface? surface = factory.getSurface(); - factory.releaseSurface(surface!); - - // If we create a new surface, it should be the same as the one we - // just created. - final Surface? newSurface = factory.getSurface(); - expect(newSurface, equals(surface)); - }); - - test('isLive', () { - final SurfaceFactory factory = SurfaceFactory(3); - - expect(factory.isLive(factory.baseSurface), isTrue); - - final Surface? surface = factory.getSurface(); - expect(factory.isLive(surface!), isTrue); - - factory.releaseSurface(surface); - expect(factory.isLive(surface), isFalse); - }); - - test('hot restart', () { - void expectDisposed(Surface surface) { - expect(surface.htmlCanvas!.isConnected, isFalse); - } - - final SurfaceFactory originalFactory = SurfaceFactory.instance; - expect(SurfaceFactory.debugUninitializedInstance, isNotNull); - - // Cause the surface and its canvas to be attached to the page - originalFactory.baseSurface.acquireFrame(const ui.Size(10, 10)); - originalFactory.baseSurface.addToScene(); - expect(originalFactory.baseSurface.htmlCanvas!.isConnected, isTrue); - - // Create a few overlay surfaces - final List overlays = []; - for (int i = 0; i < 3; i++) { - overlays.add(originalFactory.getSurface()! - ..acquireFrame(const ui.Size(10, 10)) - ..addToScene()); - } - expect(originalFactory.debugSurfaceCount, 4); - - // Trigger hot restart clean-up logic and check that we indeed clean up. - debugEmulateHotRestart(); - expect(SurfaceFactory.debugUninitializedInstance, isNull); - expectDisposed(originalFactory.baseSurface); - overlays.forEach(expectDisposed); - expect(originalFactory.debugSurfaceCount, 1); - }); - }); -} diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index b4f55330f9ea5..06e0d04ed1bbe 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -23,17 +23,14 @@ void testMain() { }); test('Surface allocates canvases efficiently', () { - final Surface? surface = SurfaceFactory.instance.getSurface(); + final Surface surface = Surface(); final CkSurface originalSurface = - surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; - final DomCanvasElement original = surface.htmlCanvas!; + surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; + final DomOffscreenCanvas original = surface.debugOffscreenCanvas!; // Expect exact requested dimensions. expect(original.width, 9); expect(original.height, 19); - expect(original.style.width, '9px'); - expect(original.style.height, '19px'); - expect(original.style.transform, _isTranslate('0', '0')); expect(originalSurface.width(), 9); expect(originalSurface.height(), 19); @@ -41,11 +38,8 @@ void testMain() { // Skia renders into the visible area. final CkSurface shrunkSurface = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk = surface.htmlCanvas!; + final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!; expect(shrunk, same(original)); - expect(shrunk.style.width, '9px'); - expect(shrunk.style.height, '19px'); - expect(shrunk.style.transform, _isTranslate('0', '-4')); expect(shrunkSurface, isNot(same(originalSurface))); expect(shrunkSurface.width(), 5); expect(shrunkSurface.height(), 15); @@ -54,52 +48,42 @@ void testMain() { // by 40% to accommodate future increases. final CkSurface firstIncreaseSurface = surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomCanvasElement firstIncrease = surface.htmlCanvas!; + final DomOffscreenCanvas firstIncrease = surface.debugOffscreenCanvas!; expect(firstIncrease, same(original)); expect(firstIncreaseSurface, isNot(same(shrunkSurface))); // Expect overallocated dimensions expect(firstIncrease.width, 14); expect(firstIncrease.height, 28); - expect(firstIncrease.style.width, '14px'); - expect(firstIncrease.style.height, '28px'); - expect(firstIncrease.style.transform, _isTranslate('0', '-8')); expect(firstIncreaseSurface.width(), 10); expect(firstIncreaseSurface.height(), 20); // Subsequent increases within 40% reuse the old canvas. final CkSurface secondIncreaseSurface = surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; - final DomCanvasElement secondIncrease = surface.htmlCanvas!; + final DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!; expect(secondIncrease, same(firstIncrease)); - expect(secondIncrease.style.transform, _isTranslate('0', '-6')); expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); expect(secondIncreaseSurface.width(), 11); expect(secondIncreaseSurface.height(), 22); // Increases beyond the 40% limit will cause a new allocation. final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; - final DomCanvasElement huge = surface.htmlCanvas!; + final DomOffscreenCanvas huge = surface.debugOffscreenCanvas!; expect(huge, same(secondIncrease)); expect(hugeSurface, isNot(same(secondIncreaseSurface))); // Also over-allocated expect(huge.width, 28); expect(huge.height, 56); - expect(huge.style.width, '28px'); - expect(huge.style.height, '56px'); - expect(huge.style.transform, _isTranslate('0', '-16')); expect(hugeSurface.width(), 20); expect(hugeSurface.height(), 40); // Shrink again. Reuse the last allocated surface. final CkSurface shrunkSurface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk2 = surface.htmlCanvas!; + final DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!; expect(shrunk2, same(huge)); - expect(shrunk2.style.width, '28px'); - expect(shrunk2.style.height, '56px'); - expect(shrunk2.style.transform, _isTranslate('0', '-41')); expect(shrunkSurface2, isNot(same(hugeSurface))); expect(shrunkSurface2.width(), 5); expect(shrunkSurface2.height(), 15); @@ -109,11 +93,8 @@ void testMain() { window.debugOverrideDevicePixelRatio(2.0); final CkSurface dpr2Surface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement dpr2Canvas = surface.htmlCanvas!; + final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!; expect(dpr2Canvas, same(huge)); - expect(dpr2Canvas.style.width, '14px'); - expect(dpr2Canvas.style.height, '28px'); - expect(dpr2Canvas.style.transform, _isTranslate('0', '-20.5')); expect(dpr2Surface2, isNot(same(hugeSurface))); expect(dpr2Surface2.width(), 5); expect(dpr2Surface2.height(), 15); @@ -123,13 +104,13 @@ void testMain() { // which cannot be a different size from the canvas. // TODO(hterkelsen): See if we can give a custom size for software // surfaces. - }, skip: isFirefox); + }, skip: isFirefox || !Surface.offscreenCanvasSupported); test( 'Surface creates new context when WebGL context is restored', () async { - final Surface? surface = SurfaceFactory.instance.getSurface(); - expect(surface!.debugForceNewContext, isTrue); + final Surface surface = Surface(); + expect(surface.debugForceNewContext, isTrue); final CkSurface before = surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; expect(surface.debugForceNewContext, isFalse); @@ -142,8 +123,7 @@ void testMain() { expect(afterAcquireFrame, same(before)); // Emulate WebGL context loss. - final DomCanvasElement canvas = - surface.htmlElement.children.single as DomCanvasElement; + final DomOffscreenCanvas canvas = surface.debugOffscreenCanvas!; final Object ctx = canvas.getContext('webgl2')!; final Object loseContextExtension = js_util.callMethod( ctx, @@ -172,7 +152,7 @@ void testMain() { expect(afterContextLost, isNot(same(before))); }, // Firefox can't create a WebGL2 context in headless mode. - skip: isFirefox, + skip: isFirefox || !Surface.offscreenCanvasSupported, ); // Regression test for https://github.com/flutter/flutter/issues/75286 @@ -183,9 +163,8 @@ void testMain() { expect(original.width(), 10); expect(original.height(), 16); - expect(surface.htmlCanvas!.style.width, '10px'); - expect(surface.htmlCanvas!.style.height, '16px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); // Increase device-pixel ratio: this makes CSS pixels bigger, so we need // fewer of them to cover the browser window. @@ -194,9 +173,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(highDpr.width(), 10); expect(highDpr.height(), 16); - expect(surface.htmlCanvas!.style.width, '5px'); - expect(surface.htmlCanvas!.style.height, '8px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need // more of them to cover the browser window. @@ -205,9 +183,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(lowDpr.width(), 10); expect(lowDpr.height(), 16); - expect(surface.htmlCanvas!.style.width, '20px'); - expect(surface.htmlCanvas!.style.height, '32px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); @@ -215,28 +192,10 @@ void testMain() { surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; expect(changeRatioAndSize.width(), 10); expect(changeRatioAndSize.height(), 16); - expect(surface.htmlCanvas!.style.width, '5px'); - expect(surface.htmlCanvas!.style.height, '8px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); - }); - }); -} - -/// Checks that the CSS 'transform' property is a translation in a cross-browser way. -/// -/// Takes strings directly to avoid issues with floating point or differences -/// in stringification of numeric values across JS and Wasm targets. -Matcher _isTranslate(String x, String y) { - // When the y coordinate is zero, Firefox omits it, e.g.: - // Chrome/Safari/Edge: translate(0px, 0px) - // Firefox: translate(0px) - final String fullFormat = 'translate(${x}px, ${y}px)'; - if (y != '0') { - return equals(fullFormat); - } else { - return anyOf( - fullFormat, // Non-Firefox browsers use this format. - 'translate(${x}px)', // Firefox omits y when it's zero. + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); + }, + skip: !Surface.offscreenCanvasSupported, ); - } + }); } diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 2b83d1e53d40d..87f332f9d2f50 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -19,9 +18,8 @@ void main() { } class StubPictureRenderer implements PictureRenderer { - final DomCanvasElement scratchCanvasElement = createDomCanvasElement( - width: 500, height: 500 - ); + final DomCanvasElement scratchCanvasElement = + createDomCanvasElement(width: 500, height: 500); @override Future renderPicture(ScenePicture picture) async { @@ -63,9 +61,11 @@ void testMain() { final List children = sceneElement.children.toList(); expect(children.length, 1); final DomElement containerElement = children.first; - expect(containerElement.tagName, equalsIgnoringCase('flt-canvas-container')); + expect( + containerElement.tagName, equalsIgnoringCase('flt-canvas-container')); - final List containerChildren = containerElement.children.toList(); + final List containerChildren = + containerElement.children.toList(); expect(containerChildren.length, 1); final DomElement canvasElement = containerChildren.first; final DomCSSStyleDeclaration style = canvasElement.style; @@ -81,12 +81,11 @@ void testMain() { debugOverrideDevicePixelRatio(2.0); final PlatformView platformView = PlatformView( - 1, - const ui.Size(100, 120), - const PlatformViewStyling( - position: PlatformViewPosition.offset(ui.Offset(50, 80)), - ) - ); + 1, + const ui.Size(100, 120), + const PlatformViewStyling( + position: PlatformViewPosition.offset(ui.Offset(50, 80)), + )); final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PlatformViewSlice([platformView], null)); final EngineScene scene = EngineScene(rootLayer); @@ -96,7 +95,8 @@ void testMain() { final List children = sceneElement.children.toList(); expect(children.length, 1); final DomElement containerElement = children.first; - expect(containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot')); + expect( + containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot')); final DomCSSStyleDeclaration style = containerElement.style; expect(style.left, '25px'); From facee980508fea776a92efca2c3276b87145d52a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 24 Oct 2023 11:28:14 -0700 Subject: [PATCH 2/4] Fix bad merge --- lib/web_ui/lib/src/engine/dom.dart | 46 +++++++++------------ lib/web_ui/test/engine/scene_view_test.dart | 1 + 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 23e055afc2bef..4e9725df0a7c6 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -199,34 +199,26 @@ external DomIntl get domIntl; external DomSymbol get domSymbol; @JS('createImageBitmap') -external JSPromise _createImageBitmap(JSAny source); -Future createImageBitmap(JSAny source) => - js_util.promiseToFuture(_createImageBitmap(source)); - -@JS('createImageBitmap') -external JSPromise _createSizedImageBitmap(DomCanvasElement canvas, JSNumber sx, - JSNumber sy, JSNumber sw, JSNumber sh); -Future createSizedImageBitmap( - DomCanvasElement canvas, int sx, int sy, int sw, int sh) => - js_util.promiseToFuture( - _createSizedImageBitmap(canvas, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); - -@JS('createImageBitmap') -external JSPromise _createSizedImageBitmapFromImageData( - DomImageData imageData, JSNumber sx, JSNumber sy, JSNumber sw, JSNumber sh); -Future createSizedImageBitmapFromImageData( - DomImageData imageData, int sx, int sy, int sw, int sh) => - js_util.promiseToFuture( - _createSizedImageBitmapFromImageData( - imageData, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); - +external JSPromise _createImageBitmap1( + JSAny source, +); @JS('createImageBitmap') -external JSPromise _createSizedOffscreenImageBitmap(DomOffscreenCanvas canvas, - JSNumber sx, JSNumber sy, JSNumber sw, JSNumber sh); -Future createSizedOffscreenImageBitmap( - DomOffscreenCanvas canvas, int sx, int sy, int sw, int sh) => - js_util.promiseToFuture(_createSizedOffscreenImageBitmap( - canvas, sx.toJS, sy.toJS, sw.toJS, sh.toJS)); +external JSPromise _createImageBitmap2( + JSAny source, + JSNumber x, + JSNumber y, + JSNumber width, + JSNumber height, +); +JSPromise createImageBitmap(JSAny source, + [({int x, int y, int width, int height})? bounds]) { + if (bounds != null) { + return _createImageBitmap2(source, bounds.x.toJS, bounds.y.toJS, + bounds.width.toJS, bounds.height.toJS); + } else { + return _createImageBitmap1(source); + } +} @JS() @staticInterop diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 87f332f9d2f50..7e29721cf12e7 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; From 1d1fa0000de64634e99afa8d618cd3fe2ea286f7 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 24 Oct 2023 12:28:12 -0700 Subject: [PATCH 3/4] Use new createImageBitmap API --- .../lib/src/engine/canvaskit/surface.dart | 26 +++++++++---------- .../test/canvaskit/render_canvas_test.dart | 15 ++++++----- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index db2277c65ac6b..6035424e2f7d3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -116,21 +116,19 @@ class Surface { DomImageBitmap bitmap; if (Surface.offscreenCanvasSupported) { - bitmap = (await createSizedOffscreenImageBitmap( - _offscreenCanvas!, - 0, - _pixelHeight - frameSize.height.toInt(), - frameSize.width.toInt(), - frameSize.height.toInt(), - ))!; + bitmap = (await createImageBitmap(_offscreenCanvas!, ( + x: 0, + y: _pixelHeight - frameSize.height.toInt(), + width: frameSize.width.toInt(), + height: frameSize.height.toInt(), + )).toDart)! as DomImageBitmap; } else { - bitmap = (await createSizedImageBitmap( - _canvasElement!, - 0, - _pixelHeight - frameSize.height.toInt(), - frameSize.width.toInt(), - frameSize.height.toInt(), - ))!; + bitmap = (await createImageBitmap(_canvasElement!, ( + x: 0, + y: _pixelHeight - frameSize.height.toInt(), + width: frameSize.width.toInt(), + height: frameSize.height.toInt() + )).toDart)! as DomImageBitmap; } canvas.render(bitmap); } diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index 75a0cfba2897c..202ed35edafac 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -1,6 +1,7 @@ // 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:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -20,13 +21,13 @@ void testMain() { }); Future newBitmap(int width, int height) async { - return (await createSizedImageBitmapFromImageData( - createBlankDomImageData(width, height), - 0, - 0, - width, - height, - ))!; + return (await createImageBitmap( + createBlankDomImageData(width, height) as JSAny, ( + x: 0, + y: 0, + width: width, + height: height, + )).toDart)! as DomImageBitmap; } // Regression test for https://github.com/flutter/flutter/issues/75286 From 59c5baea233a605198d3f0cff5f9a636d592f5bc Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 24 Oct 2023 12:46:10 -0700 Subject: [PATCH 4/4] Fix analysis error --- lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 57ff2ed6836ae..a5e9425c0027c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -5,7 +5,6 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show PlatformViewManager; -import '../configuration.dart'; import '../dom.dart'; import '../html/path_to_svg_clip.dart'; import '../platform_views/slots.dart';