diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4ee3bf8b0109a..f37b0279e3c8d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1970,11 +1970,10 @@ 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 @@ -4720,11 +4719,10 @@ 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 8f72042ca42ec..016117334272e 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -43,11 +43,10 @@ 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 492bbb1b50afb..22c94573fdf6f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -161,13 +161,6 @@ 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) => @@ -206,9 +199,6 @@ 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 06c6f5aa6a3fa..779ca9babb1bc 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -5,6 +5,7 @@ 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'; @@ -17,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 { @@ -30,6 +31,42 @@ 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(); @@ -49,12 +86,10 @@ class HtmlViewEmbedder { /// * The number of clipping elements used last time the view was composited. final Map _viewClipChains = {}; - /// 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 = {}; + /// 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 views that need to be recomposited into the scene on the next frame. final Set _viewsToRecomposite = {}; @@ -65,9 +100,6 @@ 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; @@ -92,10 +124,20 @@ 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.isVisible(viewId); - if (needNewOverlay) { + if (needNewOverlay && hasAvailableOverlay) { final CkPictureRecorder pictureRecorder = CkPictureRecorder(); pictureRecorder.beginRecording(ui.Offset.zero & _frameSize); _context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder); @@ -367,27 +409,26 @@ class HtmlViewEmbedder { (_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty) ? null : diffViewList(_activeCompositionOrder, _compositionOrder); - final List? overlayGroups = _updateOverlays(diffResult); - if (overlayGroups != null) { - _activeOverlayGroups = overlayGroups; - } + _updateOverlays(diffResult); assert( - _context.pictureRecorders.length >= _overlays.length, - 'There should be at least as many picture recorders ' + _context.pictureRecorders.length == _overlays.length, + 'There should be the same number of picture recorders ' '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).', ); - 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()); + + 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); pictureRecorderIndex++; + frame.submit(); } - CanvasKitRenderer.instance.rasterizer - .rasterizeToCanvas(overlay, pictures); } for (final CkPictureRecorder recorder in _context.pictureRecordersCreatedDuringPreroll) { @@ -440,7 +481,7 @@ class HtmlViewEmbedder { if (diffResult.addToBeginning) { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.insertBefore(platformViewRoot, elementToInsertBefore); - final RenderCanvas? overlay = _overlays[viewId]; + final Surface? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.insertBefore( overlay.htmlElement, elementToInsertBefore); @@ -448,7 +489,7 @@ class HtmlViewEmbedder { } else { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.append(platformViewRoot); - final RenderCanvas? overlay = _overlays[viewId]; + final Surface? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); } @@ -473,7 +514,7 @@ class HtmlViewEmbedder { } } } else { - RenderCanvasFactory.instance.removeSurfacesFromDom(); + SurfaceFactory.instance.removeSurfacesFromDom(); for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; @@ -491,7 +532,7 @@ class HtmlViewEmbedder { } final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - final RenderCanvas? overlay = _overlays[viewId]; + final Surface? overlay = _overlays[viewId]; skiaSceneHost.append(platformViewRoot); if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); @@ -527,8 +568,8 @@ class HtmlViewEmbedder { void _releaseOverlay(int viewId) { if (_overlays[viewId] != null) { - final RenderCanvas overlay = _overlays[viewId]!; - RenderCanvasFactory.instance.releaseCanvas(overlay); + final Surface overlay = _overlays[viewId]!; + SurfaceFactory.instance.releaseSurface(overlay); _overlays.remove(viewId); } } @@ -550,13 +591,13 @@ class HtmlViewEmbedder { // composition order of the current and previous frame, respectively. // // TODO(hterkelsen): Test this more thoroughly. - List? _updateOverlays(ViewListDiffResult? diffResult) { + void _updateOverlays(ViewListDiffResult? diffResult) { if (diffResult != null && diffResult.viewsToAdd.isEmpty && diffResult.viewsToRemove.isEmpty) { // The composition order has not changed, continue using the assigned // overlays. - return null; + return; } // Group platform views from their composition order. // Each group contains one visible view, and any number of invisible views @@ -565,10 +606,17 @@ 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. - RenderCanvasFactory.instance.releaseCanvases(); + SurfaceFactory.instance.releaseSurfaces(); _overlays.clear(); viewsNeedingOverlays.forEach(_initializeOverlay); } else { @@ -587,7 +635,6 @@ class HtmlViewEmbedder { .forEach(_initializeOverlay); } assert(_overlays.length == viewsNeedingOverlays.length); - return overlayGroups; } // Group the platform views into "overlay groups". These are sublists @@ -599,8 +646,12 @@ 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]; @@ -609,10 +660,8 @@ class HtmlViewEmbedder { currentGroup.add(view); } else { // `view` is visible. - 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. + if (!currentGroup.hasVisibleView) { + // If `view` is the first visible one of the group, add it. currentGroup.add(view, visible: true); } else { // There's already a visible `view` in `currentGroup`, so a new @@ -622,8 +671,17 @@ class HtmlViewEmbedder { // We only care about groups that have one visible view. result.add(currentGroup); } - currentGroup = OverlayGroup(); - currentGroup.add(view, visible: true); + // 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; + } } } } @@ -638,7 +696,8 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas(); + final Surface overlay = SurfaceFactory.instance.getSurface()!; + overlay.createOrUpdateSurface(_frameSize); _overlays[viewId] = overlay; } @@ -683,30 +742,29 @@ class HtmlViewEmbedder { /// Every overlay group is a list containing a visible view preceded or followed /// by zero or more invisible views. class OverlayGroup { - OverlayGroup() : _group = []; + /// Constructor + OverlayGroup( + List viewGroup, { + bool visible = false, + }) : _group = viewGroup, + _containsVisibleView = visible; // The internal list of ints. final List _group; - - /// The number of visible views in this group. - int _visibleCount = 0; + // A boolean flag to mark if any visible view has been added to the list. + bool _containsVisibleView; /// Add a [view] (maybe [visible]) to this group. void add(int view, {bool visible = false}) { _group.add(view); - if (visible) { - _visibleCount++; - } + _containsVisibleView |= visible; } /// 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 => _visibleCount > 0; - - /// Returns the number of visible views in this overlay group. - int get visibleCount => _visibleCount; + bool get hasVisibleView => _group.isNotEmpty && _containsVisibleView; } /// 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 668afeeb47434..52b8229ec2b9f 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 = RenderCanvasFactory.instance.pictureToImageSurface; + final Surface surface = SurfaceFactory.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 8d150ecae4144..67f01fff852f9 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -3,29 +3,22 @@ // 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) => - _offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + SurfaceFactory.instance.baseSurface.setSkiaResourceCacheMaxBytes(bytes); /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -36,22 +29,17 @@ class Rasterizer { return; } - _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); + 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); compositorFrame.raster(layerTree, ignoreRasterCache: true); - - CanvasKitRenderer.instance.sceneHost! - .prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement); - rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, - [pictureRecorder.endRecording()]); - + SurfaceFactory.instance.baseSurface.addToScene(); + frame.submit(); 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 deleted file mode 100644 index 7793cfbe6cbce..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ /dev/null @@ -1,113 +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: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 deleted file mode 100644 index 593390972377c..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart +++ /dev/null @@ -1,142 +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: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 db2277c65ac6b..76719d68dcea8 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -11,10 +11,11 @@ import '../configuration.dart'; import '../dom.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; +import '../window.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; -import 'picture.dart'; -import 'render_canvas.dart'; +import 'renderer.dart'; +import 'surface_factory.dart'; import 'util.dart'; // Only supported in profile/release mode. Allows Flutter to use MSAA but @@ -25,7 +26,8 @@ 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; @@ -80,16 +82,19 @@ class Surface { int? _glContext; int? _skiaCacheBytes; - /// 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 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 `` element used for this surface. + DomCanvasElement? htmlCanvas; int _pixelWidth = -1; int _pixelHeight = -1; int _sampleCount = -1; @@ -107,33 +112,7 @@ class Surface { } } - 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); - } + bool _addedToScene = false; /// Acquire a frame of the given [size] containing a drawable canvas. /// @@ -150,16 +129,21 @@ 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]. /// @@ -175,10 +159,22 @@ 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 sizeThis is the first frame we have rendered with this canvas. + // Or at least cache the estimated window size. 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) { @@ -192,6 +188,11 @@ 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!; } @@ -204,16 +205,12 @@ class Surface { final ui.Size newSize = size * 1.4; _surface?.dispose(); _surface = null; - if (Surface.offscreenCanvasSupported) { - _offscreenCanvas!.width = newSize.width; - _offscreenCanvas!.height = newSize.height; - } else { - _canvasElement!.width = newSize.width; - _canvasElement!.height = newSize.height; - } + htmlCanvas!.width = newSize.width; + htmlCanvas!.height = newSize.height; _currentCanvasPhysicalSize = newSize; _pixelWidth = newSize.width.ceil(); _pixelHeight = newSize.height.ceil(); + _updateLogicalHtmlCanvasSize(); } } @@ -221,20 +218,57 @@ 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, @@ -248,11 +282,16 @@ class Surface { } JSVoid _contextLostListener(DomEvent event) { - assert(event.target == _offscreenCanvas || event.target == _canvasElement, + assert(event.target == htmlCanvas, 'Received a context lost event for a disposed canvas'); + final SurfaceFactory factory = SurfaceFactory.instance; _contextLost = true; - _forceNewContext = true; - event.preventDefault(); + if (factory.isLive(this)) { + _forceNewContext = true; + event.preventDefault(); + } else { + dispose(); + } } /// This function is expensive. @@ -260,32 +299,18 @@ 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 (_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; + if (this.htmlCanvas != null) { + this.htmlCanvas!.removeEventListener( + 'webglcontextrestored', + _cachedContextRestoredListener, + false, + ); + this.htmlCanvas!.removeEventListener( + 'webglcontextlost', + _cachedContextLostListener, + false, + ); + this.htmlCanvas!.remove(); _cachedContextRestoredListener = null; _cachedContextLostListener = null; } @@ -294,22 +319,25 @@ class Surface { // we ensure that the rendred picture covers the entire browser window. _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); - 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; - } + 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(); // 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. @@ -317,8 +345,7 @@ 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', @@ -334,24 +361,15 @@ class Surface { _contextLost = false; if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) { - 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(); - } + 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(); _glContext = glContext; @@ -369,38 +387,40 @@ class Surface { _syncCacheBytes(); } } + + htmlElement.append(htmlCanvas); } void _initWebglParams() { - WebGLContext gl; - if (Surface.offscreenCanvasSupported) { - gl = _offscreenCanvas!.getGlContext(webGLVersion); - } else { - gl = _canvasElement!.getGlContext(webGLVersion); - } + final WebGLContext gl = htmlCanvas!.getGlContext(webGLVersion); _sampleCount = gl.getParameter(gl.samples); _stencilBits = gl.getParameter(gl.stencilBits); } CkSurface _createNewSurface(ui.Size size) { - assert(_offscreenCanvas != null || _canvasElement != null); + assert(htmlCanvas != null); if (webGLVersion == -1) { - return _makeSoftwareCanvasSurface('WebGL support not detected'); + return _makeSoftwareCanvasSurface( + htmlCanvas!, 'WebGL support not detected'); } else if (configuration.canvasKitForceCpuOnly) { - return _makeSoftwareCanvasSurface('CPU rendering forced by application'); + return _makeSoftwareCanvasSurface( + htmlCanvas!, 'CPU rendering forced by application'); } else if (_glContext == 0) { - return _makeSoftwareCanvasSurface('Failed to initialize WebGL context'); + return _makeSoftwareCanvasSurface( + htmlCanvas!, '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('Failed to initialize WebGL surface'); + return _makeSoftwareCanvasSurface( + htmlCanvas!, 'Failed to initialize WebGL surface'); } return CkSurface(skSurface, _glContext); @@ -409,20 +429,14 @@ class Surface { static bool _didWarnAboutWebGlInitializationFailure = false; - CkSurface _makeSoftwareCanvasSurface(String reason) { + CkSurface _makeSoftwareCanvasSurface( + DomCanvasElement htmlCanvas, 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( - surface, + canvasKit.MakeSWCanvasSurface(htmlCanvas), null, ); } @@ -433,19 +447,15 @@ class Surface { } void dispose() { - _offscreenCanvas?.removeEventListener( + htmlCanvas?.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false); - _offscreenCanvas?.removeEventListener( + htmlCanvas?.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 new file mode 100644 index 0000000000000..ee5b001dd8e04 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart @@ -0,0 +1,167 @@ +// 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 1a5dff3bdd696..151e2c2bc83d7 100644 --- a/lib/web_ui/lib/src/engine/configuration.dart +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -257,10 +257,15 @@ class FlutterConfiguration { 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', ); - /// 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') + /// 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. 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 c90554b06efb4..c5b4411a97b70 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -198,36 +198,6 @@ 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 {} @@ -1434,7 +1404,7 @@ extension DomCanvasRenderingContextWebGlExtension class DomCanvasRenderingContextBitmapRenderer {} extension DomCanvasRenderingContextBitmapRendererExtension - on DomCanvasRenderingContextBitmapRenderer { + on DomCanvasRenderingContextBitmapRenderer { external void transferFromImageBitmap(DomImageBitmap bitmap); } @@ -1442,13 +1412,10 @@ 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 createBlankDomImageData(int sw, int sh) => - DomImageData._empty(sw.toJS, sh.toJS); +DomImageData createDomImageData(Object? data, int sw, int sh) => + DomImageData._(data?.toJSAnyShallow, sw.toJS, sh.toJS); extension DomImageDataExtension on DomImageData { @JS('data') @@ -1466,6 +1433,33 @@ 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 {} @@ -1508,8 +1502,7 @@ 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; } @@ -1766,7 +1759,8 @@ 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; } @@ -1776,12 +1770,10 @@ 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 @@ -2316,7 +2308,9 @@ 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 + ); } } @@ -2848,13 +2842,6 @@ 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') @@ -2868,11 +2855,6 @@ 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) => @@ -3441,8 +3423,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); } } @@ -3579,8 +3561,10 @@ 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') @@ -3589,12 +3573,11 @@ 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') @@ -3605,8 +3588,3 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry { /// Whether the current browser supports `FinalizationRegistry`. bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; - -@JS('window.OffscreenCanvas') -external JSAny? get _offscreenCanvasConstructor; - -bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null; diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 46a436db87726..361e823b6e37e 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; - RenderCanvasFactory.instance.debugClear(); + SurfaceFactory.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 bf0fd59602968..a8a2b04ce9469 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(); - RenderCanvasFactory.instance.debugClear(); + SurfaceFactory.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 49db4e0dddc63..58b34661fcb4d 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_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'; @@ -41,10 +42,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!; @@ -291,7 +292,8 @@ void testMain() { }); test('renders overlays on top of platform views', () async { - expect(RenderCanvasFactory.instance.debugCacheSize, 0); + expect(SurfaceFactory.instance.debugCacheSize, 0); + expect(configuration.canvasKitMaximumSurfaces, 8); final CkPicture testPicture = paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint()); @@ -337,8 +339,8 @@ void testMain() { _platformView, _overlay, _platformView, - _platformView, _overlay, + _platformView, ]); // Frame 2: @@ -370,7 +372,7 @@ void testMain() { ]); // Frame 4: - // Render: more platform views than max overlay count. + // Render: more platform views than max cache size. // Expect: main canvas, backup overlay, maximum overlays. await Future.delayed(Duration.zero); renderTestScene(viewCount: 16); @@ -389,6 +391,7 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, _platformView, _platformView, @@ -398,7 +401,6 @@ void testMain() { _platformView, _platformView, _platformView, - _overlay, ]); // Frame 5: @@ -475,6 +477,7 @@ 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, @@ -490,10 +493,10 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, _platformView, _platformView, - _overlay, ]); // Frame 2: @@ -501,6 +504,7 @@ 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, @@ -516,10 +520,10 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, _platformView, _platformView, - _overlay, ]); // Frame 3: @@ -542,10 +546,10 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, _platformView, _platformView, - _overlay, ]); // Frame 4: @@ -568,10 +572,10 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, _platformView, _platformView, - _overlay, ]); // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 @@ -595,7 +599,8 @@ void testMain() { ]); expect( - flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'), + flutterViewEmbedder.glassPaneElement + .querySelector('flt-platform-view'), isNotNull, ); @@ -610,14 +615,13 @@ 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', @@ -660,7 +664,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 { @@ -682,7 +686,8 @@ void testMain() { ]); expect( - flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'), + flutterViewEmbedder.glassPaneElement + .querySelector('flt-platform-view'), isNotNull, ); @@ -739,8 +744,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)); @@ -777,6 +782,121 @@ 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( @@ -837,9 +957,7 @@ 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); @@ -941,9 +1059,7 @@ 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); @@ -992,22 +1108,19 @@ 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 deleted file mode 100644 index 70aa4e6073ffa..0000000000000 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ /dev/null @@ -1,95 +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 '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 deleted file mode 100644 index 75a0cfba2897c..0000000000000 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ /dev/null @@ -1,62 +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 '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 new file mode 100644 index 0000000000000..05db21472386c --- /dev/null +++ b/lib/web_ui/test/canvaskit/surface_factory_test.dart @@ -0,0 +1,103 @@ +// 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 f01016db5ae16..e31ca43bf8079 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -23,14 +23,17 @@ void testMain() { }); test('Surface allocates canvases efficiently', () { - final Surface surface = Surface(); + final Surface? surface = SurfaceFactory.instance.getSurface(); final CkSurface originalSurface = - surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; - final DomOffscreenCanvas original = surface.debugOffscreenCanvas!; + surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; + final DomCanvasElement original = surface.htmlCanvas!; // 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); @@ -38,8 +41,11 @@ void testMain() { // Skia renders into the visible area. final CkSurface shrunkSurface = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!; + final DomCanvasElement shrunk = surface.htmlCanvas!; 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); @@ -48,42 +54,52 @@ void testMain() { // by 40% to accommodate future increases. final CkSurface firstIncreaseSurface = surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomOffscreenCanvas firstIncrease = surface.debugOffscreenCanvas!; + final DomCanvasElement firstIncrease = surface.htmlCanvas!; 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 DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!; + final DomCanvasElement secondIncrease = surface.htmlCanvas!; 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 DomOffscreenCanvas huge = surface.debugOffscreenCanvas!; + final DomCanvasElement huge = surface.htmlCanvas!; 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 DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!; + final DomCanvasElement shrunk2 = surface.htmlCanvas!; 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); @@ -93,8 +109,11 @@ void testMain() { window.debugOverrideDevicePixelRatio(2.0); final CkSurface dpr2Surface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!; + final DomCanvasElement dpr2Canvas = surface.htmlCanvas!; 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); @@ -104,13 +123,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 || !Surface.offscreenCanvasSupported); + }, skip: isFirefox); test( 'Surface creates new context when WebGL context is restored', () async { - final Surface surface = Surface(); - expect(surface.debugForceNewContext, isTrue); + final Surface? surface = SurfaceFactory.instance.getSurface(); + expect(surface!.debugForceNewContext, isTrue); final CkSurface before = surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; expect(surface.debugForceNewContext, isFalse); @@ -123,7 +142,8 @@ void testMain() { expect(afterAcquireFrame, same(before)); // Emulate WebGL context loss. - final DomOffscreenCanvas canvas = surface.debugOffscreenCanvas!; + final DomCanvasElement canvas = + surface.htmlElement.children.single as DomCanvasElement; final Object ctx = canvas.getContext('webgl2')!; final Object loseContextExtension = js_util.callMethod( ctx, @@ -152,7 +172,7 @@ void testMain() { expect(afterContextLost, isNot(same(before))); }, // Firefox can't create a WebGL2 context in headless mode. - skip: isFirefox || !Surface.offscreenCanvasSupported, + skip: isFirefox, ); // Regression test for https://github.com/flutter/flutter/issues/75286 @@ -163,8 +183,9 @@ void testMain() { expect(original.width(), 10); expect(original.height(), 16); - expect(surface.debugOffscreenCanvas!.width, 10); - expect(surface.debugOffscreenCanvas!.height, 16); + expect(surface.htmlCanvas!.style.width, '10px'); + expect(surface.htmlCanvas!.style.height, '16px'); + expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); // Increase device-pixel ratio: this makes CSS pixels bigger, so we need // fewer of them to cover the browser window. @@ -173,8 +194,9 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(highDpr.width(), 10); expect(highDpr.height(), 16); - expect(surface.debugOffscreenCanvas!.width, 10); - expect(surface.debugOffscreenCanvas!.height, 16); + expect(surface.htmlCanvas!.style.width, '5px'); + expect(surface.htmlCanvas!.style.height, '8px'); + expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need // more of them to cover the browser window. @@ -183,8 +205,9 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(lowDpr.width(), 10); expect(lowDpr.height(), 16); - expect(surface.debugOffscreenCanvas!.width, 10); - expect(surface.debugOffscreenCanvas!.height, 16); + expect(surface.htmlCanvas!.style.width, '20px'); + expect(surface.htmlCanvas!.style.height, '32px'); + expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); @@ -192,10 +215,28 @@ void testMain() { surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; expect(changeRatioAndSize.width(), 10); expect(changeRatioAndSize.height(), 16); - expect(surface.debugOffscreenCanvas!.width, 10); - expect(surface.debugOffscreenCanvas!.height, 16); - }, - skip: !Surface.offscreenCanvasSupported, - ); + 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. + ); + } +} diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index d054059c4b443..0b475d8842824 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'; @@ -18,18 +19,17 @@ 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 { final ui.Rect cullRect = picture.cullRect; - final DomImageBitmap bitmap = (await createSizedImageBitmap( - scratchCanvasElement, - 0, - 0, - cullRect.width.toInt(), - cullRect.height.toInt()))!; + final DomImageBitmap bitmap = (await createImageBitmap( + scratchCanvasElement as JSAny, + (x: 0, y: 0, width: cullRect.width.toInt(), height: cullRect.height.toInt()) + ).toDart)! as DomImageBitmap; return bitmap; } } @@ -58,11 +58,9 @@ 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; @@ -78,11 +76,12 @@ 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); @@ -92,8 +91,7 @@ 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'); diff --git a/lib/web_ui/test/ui/image_golden_test.dart b/lib/web_ui/test/ui/image_golden_test.dart index 9894ccac310a9..fccb86c8b2951 100644 --- a/lib/web_ui/test/ui/image_golden_test.dart +++ b/lib/web_ui/test/ui/image_golden_test.dart @@ -318,7 +318,7 @@ Future testMain() async { image.src = url; await completer.future; - final DomImageBitmap bitmap = (await createImageBitmap(image as JSAny))!; + final DomImageBitmap bitmap = (await createImageBitmap(image as JSAny).toDart)! as DomImageBitmap; return renderer.createImageFromImageBitmap(bitmap); }); }