diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 6b9db5d1ef6a7..07d68632542e5 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -5902,6 +5902,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart + ../.. ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/display_canvas_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views_diff.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart + ../../../flutter/LICENSE @@ -5913,8 +5914,10 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart + ../../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/offscreen_canvas_rasterizer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart + ../../../flutter/LICENSE @@ -5924,7 +5927,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.da 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 @@ -8722,6 +8724,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/display_canvas_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views_diff.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -8733,8 +8736,10 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/offscreen_canvas_rasterizer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart @@ -8744,7 +8749,6 @@ 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 diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 593bed532c250..500392ac9ac22 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -23,6 +23,7 @@ export 'engine/canvaskit/canvas.dart'; export 'engine/canvaskit/canvaskit_api.dart'; export 'engine/canvaskit/canvaskit_canvas.dart'; export 'engine/canvaskit/color_filter.dart'; +export 'engine/canvaskit/display_canvas_factory.dart'; export 'engine/canvaskit/embedded_views.dart'; export 'engine/canvaskit/embedded_views_diff.dart'; export 'engine/canvaskit/fonts.dart'; @@ -34,8 +35,10 @@ export 'engine/canvaskit/layer.dart'; export 'engine/canvaskit/layer_scene_builder.dart'; export 'engine/canvaskit/layer_tree.dart'; export 'engine/canvaskit/mask_filter.dart'; +export 'engine/canvaskit/multi_surface_rasterizer.dart'; export 'engine/canvaskit/n_way_canvas.dart'; export 'engine/canvaskit/native_memory.dart'; +export 'engine/canvaskit/offscreen_canvas_rasterizer.dart'; export 'engine/canvaskit/painting.dart'; export 'engine/canvaskit/path.dart'; export 'engine/canvaskit/path_metrics.dart'; @@ -44,7 +47,6 @@ 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'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/display_canvas_factory.dart similarity index 68% rename from lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart rename to lib/web_ui/lib/src/engine/canvaskit/display_canvas_factory.dart index e814c9e6fe7e9..93807b60bb596 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/display_canvas_factory.dart @@ -5,26 +5,30 @@ import 'package:meta/meta.dart'; import '../../engine.dart'; -/// Caches canvases used to overlay platform views. -class RenderCanvasFactory { - RenderCanvasFactory() { +/// Caches canvases used to display Skia-drawn content. +class DisplayCanvasFactory { + DisplayCanvasFactory({required this.createCanvas}) { assert(() { registerHotRestartListener(dispose); return true; }()); } + /// A function which is passed in as a constructor parameter which is used to + /// create new display canvases. + final T Function() createCanvas; + /// 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(); + late final T baseCanvas = createCanvas()..initialize(); /// Canvases created by this factory which are currently in use. - final List _liveCanvases = []; + final List _liveCanvases = []; /// Canvases created by this factory which are no longer in use. These can be /// reused. - final List _cache = []; + final List _cache = []; /// The number of canvases which have been created by this factory. int get _canvasCount => _liveCanvases.length + _cache.length + 1; @@ -38,15 +42,16 @@ class RenderCanvasFactory { /// Useful in tests. int get debugCacheSize => _cache.length; - /// Gets an overlay canvas from the cache or creates a new one if there are + /// Gets a display canvas from the cache or creates a new one if there are /// none in the cache. - RenderCanvas getCanvas() { + T getCanvas() { if (_cache.isNotEmpty) { - final RenderCanvas canvas = _cache.removeLast(); + final T canvas = _cache.removeLast(); _liveCanvases.add(canvas); return canvas; } else { - final RenderCanvas canvas = RenderCanvas(); + final T canvas = createCanvas(); + canvas.initialize(); _liveCanvases.add(canvas); return canvas; } @@ -63,27 +68,35 @@ class RenderCanvasFactory { _liveCanvases.clear(); } - /// Removes all surfaces except the base surface from the DOM. + /// Removes all canvases except the base canvas from the DOM. /// /// This is called at the beginning of the frame to prepare for painting into - /// the new surfaces. - void removeSurfacesFromDom() { + /// the new canvases. + void removeCanvasesFromDom() { _cache.forEach(_removeFromDom); + _liveCanvases.forEach(_removeFromDom); + } + + /// Calls [callback] on each canvas created by this factory. + void forEachCanvas(void Function(T canvas) callback) { + callback(baseCanvas); + _cache.forEach(callback); + _liveCanvases.forEach(callback); } // Removes [canvas] from the DOM. - void _removeFromDom(RenderCanvas canvas) { - canvas.htmlElement.remove(); + void _removeFromDom(T canvas) { + canvas.hostElement.remove(); } /// Signals that a canvas is no longer being used. It can be reused. - void releaseCanvas(RenderCanvas canvas) { + void releaseCanvas(T 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(); + canvas.hostElement.remove(); _liveCanvases.remove(canvas); _cache.add(canvas); } @@ -94,7 +107,7 @@ class RenderCanvasFactory { /// /// If a canvas is not live, then it must be in the cache and ready to be /// reused. - bool isLive(RenderCanvas canvas) { + bool isLive(T canvas) { if (canvas == baseCanvas || _liveCanvases.contains(canvas)) { return true; } @@ -104,10 +117,10 @@ class RenderCanvasFactory { /// Dispose all canvases created by this factory. void dispose() { - for (final RenderCanvas canvas in _cache) { + for (final T canvas in _cache) { canvas.dispose(); } - for (final RenderCanvas canvas in _liveCanvases) { + for (final T canvas in _liveCanvases) { canvas.dispose(); } baseCanvas.dispose(); 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 43f847caa9474..bb2b89b3ae0b9 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -18,16 +18,13 @@ import 'path.dart'; import 'picture.dart'; import 'picture_recorder.dart'; import 'rasterizer.dart'; -import 'render_canvas.dart'; -import 'render_canvas_factory.dart'; /// This composites HTML views into the [ui.Scene]. class HtmlViewEmbedder { - HtmlViewEmbedder(this.sceneHost, this.rasterizer, this.renderCanvasFactory); + HtmlViewEmbedder(this.sceneHost, this.rasterizer); final DomElement sceneHost; - final Rasterizer rasterizer; - final RenderCanvasFactory renderCanvasFactory; + final ViewRasterizer rasterizer; /// The context for the current frame. EmbedderFrameContext _context = EmbedderFrameContext(); @@ -53,7 +50,7 @@ class HtmlViewEmbedder { static const int maximumOverlays = 7; /// Canvases used to draw on top of platform views, keyed by platform view ID. - final Map _overlays = {}; + final Map _overlays = {}; /// The views that need to be recomposited into the scene on the next frame. final Set _viewsToRecomposite = {}; @@ -381,7 +378,7 @@ class HtmlViewEmbedder { int pictureRecorderIndex = 0; for (final OverlayGroup overlayGroup in _activeOverlayGroups) { - final RenderCanvas overlay = _overlays[overlayGroup.last]!; + final DisplayCanvas overlay = _overlays[overlayGroup.last]!; final List pictures = []; for (int i = 0; i < overlayGroup.visibleCount; i++) { pictures.add( @@ -441,17 +438,17 @@ class HtmlViewEmbedder { if (diffResult.addToBeginning) { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; sceneHost.insertBefore(platformViewRoot, elementToInsertBefore); - final RenderCanvas? overlay = _overlays[viewId]; + final DisplayCanvas? overlay = _overlays[viewId]; if (overlay != null) { sceneHost.insertBefore( - overlay.htmlElement, elementToInsertBefore); + overlay.hostElement, elementToInsertBefore); } } else { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; sceneHost.append(platformViewRoot); - final RenderCanvas? overlay = _overlays[viewId]; + final DisplayCanvas? overlay = _overlays[viewId]; if (overlay != null) { - sceneHost.append(overlay.htmlElement); + sceneHost.append(overlay.hostElement); } } } @@ -460,7 +457,7 @@ class HtmlViewEmbedder { for (int i = 0; i < _compositionOrder.length; i++) { final int view = _compositionOrder[i]; if (_overlays[view] != null) { - final DomElement overlayElement = _overlays[view]!.htmlElement; + final DomElement overlayElement = _overlays[view]!.hostElement; if (!overlayElement.isConnected!) { // This overlay wasn't added to the DOM. if (i == _compositionOrder.length - 1) { @@ -474,7 +471,7 @@ class HtmlViewEmbedder { } } } else { - renderCanvasFactory.removeSurfacesFromDom(); + rasterizer.removeOverlaysFromDom(); for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; @@ -492,10 +489,10 @@ class HtmlViewEmbedder { } final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - final RenderCanvas? overlay = _overlays[viewId]; + final DisplayCanvas? overlay = _overlays[viewId]; sceneHost.append(platformViewRoot); if (overlay != null) { - sceneHost.append(overlay.htmlElement); + sceneHost.append(overlay.hostElement); } _activeCompositionOrder.add(viewId); unusedViews.remove(viewId); @@ -528,8 +525,8 @@ class HtmlViewEmbedder { void _releaseOverlay(int viewId) { if (_overlays[viewId] != null) { - final RenderCanvas overlay = _overlays[viewId]!; - renderCanvasFactory.releaseCanvas(overlay); + final DisplayCanvas overlay = _overlays[viewId]!; + rasterizer.releaseOverlay(overlay); _overlays.remove(viewId); } } @@ -569,7 +566,7 @@ class HtmlViewEmbedder { 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.releaseCanvases(); + rasterizer.releaseOverlays(); _overlays.clear(); viewsNeedingOverlays.forEach(_initializeOverlay); } else { @@ -639,7 +636,7 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final RenderCanvas overlay = renderCanvasFactory.getCanvas(); + final DisplayCanvas overlay = rasterizer.getOverlay(); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart new file mode 100644 index 0000000000000..f3b2d78405bf1 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +/// A Rasterizer which uses one or many on-screen WebGL contexts to display the +/// scene. This way of rendering is prone to bugs because there is a limit to +/// how many WebGL contexts can be live at one time as well as bugs in sharing +/// GL resources between the contexts. However, using [createImageBitmap] is +/// currently very slow on Firefox and Safari browsers, so directly rendering +/// to several [Surface]s is how we can achieve 60 fps on these browsers. +class MultiSurfaceRasterizer extends Rasterizer { + @override + MultiSurfaceViewRasterizer createViewRasterizer(EngineFlutterView view) { + return _viewRasterizers.putIfAbsent( + view, () => MultiSurfaceViewRasterizer(view, this)); + } + + final Map _viewRasterizers = + {}; + + @override + void dispose() { + for (final MultiSurfaceViewRasterizer viewRasterizer + in _viewRasterizers.values) { + viewRasterizer.dispose(); + } + _viewRasterizers.clear(); + } + + @override + void setResourceCacheMaxBytes(int bytes) { + for (final MultiSurfaceViewRasterizer viewRasterizer + in _viewRasterizers.values) { + viewRasterizer.displayFactory.forEachCanvas((Surface surface) { + surface.setSkiaResourceCacheMaxBytes(bytes); + }); + } + } +} + +class MultiSurfaceViewRasterizer extends ViewRasterizer { + MultiSurfaceViewRasterizer(super.view, this.rasterizer); + + final MultiSurfaceRasterizer rasterizer; + + @override + final DisplayCanvasFactory displayFactory = + DisplayCanvasFactory( + createCanvas: () => Surface(isDisplayCanvas: true)); + + @override + void prepareToDraw() { + displayFactory.baseCanvas.createOrUpdateSurface(currentFrameSize); + } + + @override + Future rasterizeToCanvas( + DisplayCanvas canvas, List pictures) { + final Surface surface = canvas as Surface; + surface.createOrUpdateSurface(currentFrameSize); + surface.positionToShowFrame(currentFrameSize); + final CkCanvas skCanvas = surface.getCanvas(); + skCanvas.clear(const ui.Color(0x00000000)); + pictures.forEach(skCanvas.drawPicture); + surface.flush(); + return Future.value(); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/offscreen_canvas_rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/offscreen_canvas_rasterizer.dart new file mode 100644 index 0000000000000..585575391a1cb --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/offscreen_canvas_rasterizer.dart @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/src/engine.dart'; + +/// A [Rasterizer] that uses a single GL context in an OffscreenCanvas to do +/// all the rendering. It transfers bitmaps created in the OffscreenCanvas to +/// one or many on-screen elements to actually display the scene. +class OffscreenCanvasRasterizer extends Rasterizer { + /// 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(); + + @override + OffscreenCanvasViewRasterizer createViewRasterizer(EngineFlutterView view) { + return _viewRasterizers.putIfAbsent( + view, () => OffscreenCanvasViewRasterizer(view, this)); + } + + final Map _viewRasterizers = + {}; + + @override + void setResourceCacheMaxBytes(int bytes) { + offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + } + + @override + void dispose() { + offscreenSurface.dispose(); + for (final OffscreenCanvasViewRasterizer viewRasterizer + in _viewRasterizers.values) { + viewRasterizer.dispose(); + } + } +} + +class OffscreenCanvasViewRasterizer extends ViewRasterizer { + OffscreenCanvasViewRasterizer(super.view, this.rasterizer); + + final OffscreenCanvasRasterizer rasterizer; + + @override + final DisplayCanvasFactory displayFactory = + DisplayCanvasFactory(createCanvas: () => RenderCanvas()); + + /// Render the given [pictures] so it is displayed by the given [canvas]. + @override + Future rasterizeToCanvas( + DisplayCanvas canvas, List pictures) async { + await rasterizer.offscreenSurface.rasterizeToCanvas( + currentFrameSize, + canvas as RenderCanvas, + pictures, + ); + } + + @override + void prepareToDraw() { + rasterizer.offscreenSurface.createOrUpdateSurface(currentFrameSize); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 4e987d3a517f8..650b5bea80ea3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -5,31 +5,40 @@ import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -/// A class that can rasterize [LayerTree]s into a given `sceneHost` element. -class Rasterizer { - Rasterizer(this.view); +abstract class Rasterizer { + /// Creates a [ViewRasterizer] for a given [view]. + ViewRasterizer createViewRasterizer(EngineFlutterView view); + /// Sets the maximum size of the resource cache to [bytes]. + void setResourceCacheMaxBytes(int bytes); + + /// Disposes this rasterizer and all [ViewRasterizer]s that it created. + void dispose(); +} + +abstract class ViewRasterizer { + ViewRasterizer(this.view); + + /// The view this rasterizer renders into. final EngineFlutterView view; - DomElement get sceneHost => view.dom.sceneHost; + + /// The size of the current frame being rasterized. + ui.Size currentFrameSize = ui.Size.zero; + + /// The context which is persisted between frames. final CompositorContext context = CompositorContext(); - final RenderCanvasFactory renderCanvasFactory = RenderCanvasFactory(); - late final HtmlViewEmbedder viewEmbedder = - HtmlViewEmbedder(sceneHost, this, renderCanvasFactory); - ui.Size _currentFrameSize = ui.Size.zero; + /// The platform view embedder. + late final HtmlViewEmbedder viewEmbedder = HtmlViewEmbedder(sceneHost, this); - /// Render the given [pictures] so it is displayed by the given [canvas]. - Future rasterizeToCanvas( - RenderCanvas canvas, List pictures) async { - await CanvasKitRenderer.instance.offscreenSurface.rasterizeToCanvas( - _currentFrameSize, - canvas, - pictures, - ); - } + /// A factory for creating overlays. + DisplayCanvasFactory get displayFactory; + + /// The scene host which this rasterizer should raster into. + DomElement get sceneHost => view.dom.sceneHost; - /// Creates a new frame from this rasterizer's surface, draws the given - /// [LayerTree] into it, and then submits the frame. + /// Draws the [layerTree] to the screen for the view associated with this + /// rasterizer. Future draw(LayerTree layerTree) async { final ui.Size frameSize = view.physicalSize; if (frameSize.isEmpty) { @@ -37,27 +46,78 @@ class Rasterizer { return; } - _currentFrameSize = frameSize; - CanvasKitRenderer.instance.offscreenSurface.acquireFrame(_currentFrameSize); - viewEmbedder.frameSize = _currentFrameSize; + currentFrameSize = frameSize; + prepareToDraw(); + viewEmbedder.frameSize = currentFrameSize; final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); + pictureRecorder.beginRecording(ui.Offset.zero & currentFrameSize); pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); final Frame compositorFrame = context.acquireFrame(pictureRecorder.recordingCanvas!, viewEmbedder); compositorFrame.raster(layerTree, ignoreRasterCache: true); - sceneHost.prepend(renderCanvasFactory.baseCanvas.htmlElement); - await rasterizeToCanvas(renderCanvasFactory.baseCanvas, - [pictureRecorder.endRecording()]); + sceneHost.prepend(displayFactory.baseCanvas.hostElement); + await rasterizeToCanvas( + displayFactory.baseCanvas, [pictureRecorder.endRecording()]); await viewEmbedder.submitFrame(); } - /// Disposes of this rasterizer. + /// Do some initialization to prepare to draw a frame. + /// + /// For example, in the [OffscreenCanvasRasterizer], this ensures the backing + /// [OffscreenCanvas] is the correct size to draw the frame. + void prepareToDraw(); + + /// Rasterize the [pictures] to the given [canvas]. + Future rasterizeToCanvas( + DisplayCanvas canvas, List pictures); + + /// Get a [DisplayCanvas] to use as an overlay. + DisplayCanvas getOverlay() { + return displayFactory.getCanvas(); + } + + /// Release the given [overlay] so it may be reused. + void releaseOverlay(DisplayCanvas overlay) { + displayFactory.releaseCanvas(overlay); + } + + /// Release all overlays. + void releaseOverlays() { + displayFactory.releaseCanvases(); + } + + /// Remove all overlays that have been created from the DOM. + void removeOverlaysFromDom() { + displayFactory.removeCanvasesFromDom(); + } + + /// Disposes this rasterizer. void dispose() { viewEmbedder.dispose(); - renderCanvasFactory.dispose(); + displayFactory.dispose(); } } + +/// A [DisplayCanvas] is an abstraction for a canvas element which displays +/// Skia-drawn pictures to the screen. They are also sometimes called "overlays" +/// because they can be overlaid on top of platform views, which are HTML +/// content that isn't rendered by Skia. +/// +/// [DisplayCanvas]es are drawn into with [ViewRasterizer.rasterizeToCanvas]. +abstract class DisplayCanvas { + /// The DOM element which, when appended to the scene host, will display the + /// Skia-rendered content to the screen. + DomElement get hostElement; + + /// Whether or not this overlay canvas is attached to the DOM. + bool get isConnected; + + /// Initialize the overlay. + void initialize(); + + /// Disposes this overlay. + void dispose(); +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart index 9308c11ac6e2b..8f5f1fc40086a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -8,6 +8,7 @@ import 'package:ui/ui.dart' as ui; import '../display.dart'; import '../dom.dart'; +import 'rasterizer.dart'; /// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit /// in the (off-screen) SkSurface which is backed by an OffscreenCanvas. @@ -26,12 +27,12 @@ import '../dom.dart'; /// 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 { +class RenderCanvas extends DisplayCanvas { RenderCanvas() { canvasElement.setAttribute('aria-hidden', 'true'); canvasElement.style.position = 'absolute'; _updateLogicalHtmlCanvasSize(); - htmlElement.append(canvasElement); + hostElement.append(canvasElement); } /// The root HTML element for this canvas. @@ -43,7 +44,8 @@ class RenderCanvas { /// 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'); + @override + final DomElement hostElement = createDomElement('flt-canvas-container'); /// The underlying `` element used to display the pixels. final DomCanvasElement canvasElement = createDomCanvasElement(); @@ -68,7 +70,8 @@ class RenderCanvas { /// match the size of the window precisely we use the most precise floating /// point value we can get. void _updateLogicalHtmlCanvasSize() { - final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio; + final double devicePixelRatio = + EngineFlutterDisplay.instance.devicePixelRatio; final double logicalWidth = _pixelWidth / devicePixelRatio; final double logicalHeight = _pixelHeight / devicePixelRatio; final DomCSSStyleDeclaration style = canvasElement.style; @@ -113,7 +116,8 @@ class RenderCanvas { size.height.ceil() == _pixelHeight) { // The existing canvas doesn't need to be resized (unless the device pixel // ratio changed). - if (EngineFlutterDisplay.instance.devicePixelRatio != _currentDevicePixelRatio) { + if (EngineFlutterDisplay.instance.devicePixelRatio != + _currentDevicePixelRatio) { _updateLogicalHtmlCanvasSize(); } return; @@ -130,7 +134,16 @@ class RenderCanvas { _updateLogicalHtmlCanvasSize(); } + @override + bool get isConnected => canvasElement.isConnected!; + + @override + void initialize() { + // No extra initialization needed. + } + + @override void dispose() { - htmlElement.remove(); + hostElement.remove(); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index deb63b4b159bf..3aba5572cdb6c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -44,12 +44,17 @@ class CanvasKitRenderer implements Renderer { DomElement? _sceneHost; DomElement? get sceneHost => _sceneHost; - /// 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(); + final Rasterizer _rasterizer = _createRasterizer(); + + static Rasterizer _createRasterizer() { + if (isSafari || isFirefox) { + return MultiSurfaceRasterizer(); + } + return OffscreenCanvasRasterizer(); + } set resourceCacheMaxBytes(int bytes) => - offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + _rasterizer.setResourceCacheMaxBytes(bytes); /// A surface used specifically for `Picture.toImage` when software rendering /// is supported. @@ -415,19 +420,19 @@ class CanvasKitRenderer implements Renderer { assert(_rasterizers.containsKey(view.viewId), "Unable to render to a view which hasn't been registered"); - final Rasterizer rasterizer = _rasterizers[view.viewId]!; + final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; await rasterizer.draw((scene as LayerScene).layerTree); frameTimingsOnRasterFinish(); } // Map from view id to the associated Rasterizer for that view. - final Map _rasterizers = {}; + final Map _rasterizers = {}; void _onViewCreated(int viewId) { final EngineFlutterView view = EnginePlatformDispatcher.instance.viewManager[viewId]!; - _rasterizers[view.viewId] = Rasterizer(view); + _rasterizers[view.viewId] = _rasterizer.createViewRasterizer(view); } void _onViewDisposed(int viewId) { @@ -435,11 +440,11 @@ class CanvasKitRenderer implements Renderer { if (!_rasterizers.containsKey(viewId)) { return; } - final Rasterizer rasterizer = _rasterizers.remove(viewId)!; + final ViewRasterizer rasterizer = _rasterizers.remove(viewId)!; rasterizer.dispose(); } - Rasterizer? debugGetRasterizerForView(EngineFlutterView view) { + ViewRasterizer? debugGetRasterizerForView(EngineFlutterView view) { return _rasterizers[view.viewId]; } @@ -447,7 +452,7 @@ class CanvasKitRenderer implements Renderer { void dispose() { _onViewCreatedListener?.cancel(); _onViewDisposedListener?.cancel(); - for (final Rasterizer rasterizer in _rasterizers.values) { + for (final ViewRasterizer rasterizer in _rasterizers.values) { rasterizer.dispose(); } _rasterizers.clear(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index d3e1b8f9f150a..a6cff70ea730b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -8,12 +8,14 @@ import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; import '../configuration.dart'; +import '../display.dart'; import '../dom.dart'; import '../platform_dispatcher.dart'; import '../util.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; import 'picture.dart'; +import 'rasterizer.dart'; import 'render_canvas.dart'; import 'util.dart'; @@ -47,11 +49,19 @@ class SurfaceFrame { /// The underlying representation is a [CkSurface], which can be reused by /// successive frames if they are the same size. Otherwise, a new [CkSurface] is /// created. -class Surface { - Surface(); +class Surface extends DisplayCanvas { + Surface({this.isDisplayCanvas = false}) + : useOffscreenCanvas = + Surface.offscreenCanvasSupported && !isDisplayCanvas; CkSurface? _surface; + /// Whether or not to use an `OffscreenCanvas` to back this [Surface]. + final bool useOffscreenCanvas; + + /// If `true`, this [Surface] is used as a [DisplayCanvas]. + final bool isDisplayCanvas; + /// If true, forces a new WebGL context to be created, even if the window /// size is the same. This is used to restore the UI after the browser tab /// goes dormant and loses the GL context. @@ -90,8 +100,14 @@ class Surface { /// supported. DomCanvasElement? _canvasElement; + /// Note, if this getter is called, then this Surface is being used as an + /// overlay and must be backed by an onscreen element. + @override + final DomElement hostElement = createDomElement('flt-canvas-container'); + int _pixelWidth = -1; int _pixelHeight = -1; + double _currentDevicePixelRatio = -1; int _sampleCount = -1; int _stencilBits = -1; @@ -107,16 +123,25 @@ class Surface { } } + /// The CanvasKit canvas associated with this surface. + CkCanvas getCanvas() { + return _surface!.getCanvas(); + } + + void flush() { + _surface!.flush(); + } + Future rasterizeToCanvas( ui.Size frameSize, RenderCanvas canvas, List pictures) async { - final CkCanvas skCanvas = _surface!.getCanvas(); + final CkCanvas skCanvas = getCanvas(); skCanvas.clear(const ui.Color(0x00000000)); pictures.forEach(skCanvas.drawPicture); - _surface!.flush(); + flush(); if (browserSupportsCreateImageBitmap) { JSObject bitmapSource; - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { bitmapSource = _offscreenCanvas! as JSObject; } else { bitmapSource = _canvasElement! as JSObject; @@ -132,7 +157,7 @@ class Surface { // If the browser doesn't support `createImageBitmap` (e.g. Safari 14) // then render using `drawImage` instead. DomCanvasImageSource imageSource; - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { imageSource = _offscreenCanvas! as DomCanvasImageSource; } else { imageSource = _canvasElement! as DomCanvasImageSource; @@ -159,6 +184,44 @@ class Surface { ui.Size? _currentCanvasPhysicalSize; ui.Size? _currentSurfaceSize; + /// 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 devicePixelRatio = + EngineFlutterDisplay.instance.devicePixelRatio; + final double logicalWidth = _pixelWidth / devicePixelRatio; + final double logicalHeight = _pixelHeight / devicePixelRatio; + final DomCSSStyleDeclaration style = _canvasElement!.style; + style.width = '${logicalWidth}px'; + style.height = '${logicalHeight}px'; + _currentDevicePixelRatio = devicePixelRatio; + } + + /// The element backing this surface may be larger than the screen. + /// The Surface will draw the frame to the bottom left of the , but + /// the is, by default, positioned so that the top left corner is in + /// the top left of the window. We need to shift the canvas down so that the + /// bottom left of the is the the bottom left corner of the window. + void positionToShowFrame(ui.Size frameSize) { + assert(isDisplayCanvas, + 'Should not position Surface if not used as a render canvas'); + final double devicePixelRatio = + EngineFlutterDisplay.instance.devicePixelRatio; + final double logicalHeight = _pixelHeight / devicePixelRatio; + final double logicalFrameHeight = frameSize.height / devicePixelRatio; + + // Shift the canvas up so the bottom left is in the window. + _canvasElement!.style.transform = + 'translate(0px, ${logicalFrameHeight - logicalHeight}px)'; + } + /// This is only valid after the first frame or if [ensureSurface] has been /// called bool get usingSoftwareBackend => @@ -198,6 +261,11 @@ class Surface { if (previousSurfaceSize != null && size.width == previousSurfaceSize.width && size.height == previousSurfaceSize.height) { + final double devicePixelRatio = + EngineFlutterDisplay.instance.devicePixelRatio; + if (isDisplayCanvas && devicePixelRatio != _currentDevicePixelRatio) { + _updateLogicalHtmlCanvasSize(); + } return _surface!; } @@ -210,16 +278,20 @@ 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; - } - _currentCanvasPhysicalSize = newSize; _pixelWidth = newSize.width.ceil(); _pixelHeight = newSize.height.ceil(); + if (useOffscreenCanvas) { + _offscreenCanvas!.width = _pixelWidth.toDouble(); + _offscreenCanvas!.height = _pixelHeight.toDouble(); + } else { + _canvasElement!.width = _pixelWidth.toDouble(); + _canvasElement!.height = _pixelHeight.toDouble(); + } + _currentCanvasPhysicalSize = + ui.Size(_pixelWidth.toDouble(), _pixelHeight.toDouble()); + if (isDisplayCanvas) { + _updateLogicalHtmlCanvasSize(); + } } } @@ -291,6 +363,7 @@ class Surface { _cachedContextLostListener, false, ); + _canvasElement!.remove(); _canvasElement = null; _cachedContextRestoredListener = null; _cachedContextLostListener = null; @@ -301,7 +374,7 @@ class Surface { _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); DomEventTarget htmlCanvas; - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas( _pixelWidth, _pixelHeight, @@ -315,6 +388,12 @@ class Surface { htmlCanvas = canvas; _canvasElement = canvas; _offscreenCanvas = null; + if (isDisplayCanvas) { + _canvasElement!.setAttribute('aria-hidden', 'true'); + _canvasElement!.style.position = 'absolute'; + hostElement.append(_canvasElement!); + _updateLogicalHtmlCanvasSize(); + } } // When the browser tab using WebGL goes dormant the browser and/or OS may @@ -347,7 +426,7 @@ class Surface { antialias: _kUsingMSAA ? 1 : 0, majorVersion: webGLVersion.toDouble(), ); - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { glContext = canvasKit.GetOffscreenWebGLContext( _offscreenCanvas!, options, @@ -379,7 +458,7 @@ class Surface { void _initWebglParams() { WebGLContext gl; - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { gl = _offscreenCanvas!.getGlContext(webGLVersion); } else { gl = _canvasElement!.getGlContext(webGLVersion); @@ -399,8 +478,8 @@ class Surface { } else { final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, - size.width.roundToDouble(), - size.height.roundToDouble(), + size.width.ceilToDouble(), + size.height.ceilToDouble(), SkColorSpaceSRGB, _sampleCount, _stencilBits); @@ -422,7 +501,7 @@ class Surface { } SkSurface surface; - if (Surface.offscreenCanvasSupported) { + if (useOffscreenCanvas) { surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!); } else { surface = canvasKit.MakeSWCanvasSurface(_canvasElement!); @@ -438,6 +517,15 @@ class Surface { return true; } + @override + bool get isConnected => _canvasElement!.isConnected!; + + @override + void initialize() { + ensureSurface(); + } + + @override void dispose() { _offscreenCanvas?.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false); @@ -478,8 +566,8 @@ class CkSurface { int? get context => _glContext; - int width() => surface.width().round(); - int height() => surface.height().round(); + int width() => surface.width().ceil(); + int height() => surface.height().ceil(); void dispose() { if (_isDisposed) { diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/display_canvas_factory_test.dart similarity index 55% rename from lib/web_ui/test/canvaskit/render_canvas_factory_test.dart rename to lib/web_ui/test/canvaskit/display_canvas_factory_test.dart index fb5f04fbac7a7..ea2d4b1ea7fbe 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ b/lib/web_ui/test/canvaskit/display_canvas_factory_test.dart @@ -12,48 +12,70 @@ void main() { internalBootstrapBrowserTest(() => testMain); } +class DummyDisplayCanvas extends DisplayCanvas { + @override + void dispose() {} + + final DomElement _element = createDomElement('div'); + + @override + DomElement get hostElement => _element; + + @override + void initialize() {} + + @override + bool get isConnected => throw UnimplementedError(); +} + void testMain() { - group('$RenderCanvasFactory', () { + group('$DisplayCanvasFactory', () { setUpCanvasKitTest(); test('getCanvas', () { - final RenderCanvasFactory factory = RenderCanvasFactory(); + final DisplayCanvasFactory factory = + DisplayCanvasFactory( + createCanvas: () => DummyDisplayCanvas()); expect(factory.baseCanvas, isNotNull); expect(factory.debugSurfaceCount, equals(1)); // Get a canvas from the factory, it should be unique. - final RenderCanvas newCanvas = factory.getCanvas(); + final DisplayCanvas 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(); + final DisplayCanvas anotherCanvas = factory.getCanvas(); expect(anotherCanvas, isNot(equals(factory.baseCanvas))); expect(factory.debugSurfaceCount, equals(3)); }); test('releaseCanvas', () { - final RenderCanvasFactory factory = RenderCanvasFactory(); + final DisplayCanvasFactory factory = + DisplayCanvasFactory( + createCanvas: () => DummyDisplayCanvas()); // Create a new canvas and immediately release it. - final RenderCanvas canvas = factory.getCanvas(); + final DisplayCanvas 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(); + final DisplayCanvas newCanvas = factory.getCanvas(); expect(newCanvas, equals(canvas)); }); test('isLive', () { - final RenderCanvasFactory factory = RenderCanvasFactory(); + final DisplayCanvasFactory factory = + DisplayCanvasFactory( + createCanvas: () => DummyDisplayCanvas()); expect(factory.isLive(factory.baseCanvas), isTrue); - final RenderCanvas canvas = factory.getCanvas(); + final DisplayCanvas canvas = factory.getCanvas(); expect(factory.isLive(canvas), isTrue); factory.releaseCanvas(canvas); @@ -61,27 +83,28 @@ void testMain() { }); test('hot restart', () { - void expectDisposed(RenderCanvas canvas) { - expect(canvas.canvasElement.isConnected, isFalse); + void expectDisposed(DisplayCanvas canvas) { + expect(canvas.isConnected, isFalse); } final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; - final RenderCanvasFactory originalFactory = CanvasKitRenderer.instance - .debugGetRasterizerForView(implicitView)! - .renderCanvasFactory; + final DisplayCanvasFactory originalFactory = + CanvasKitRenderer.instance + .debugGetRasterizerForView(implicitView)! + .displayFactory; // Cause the surface and its canvas to be attached to the page implicitView.dom.sceneHost - .prepend(originalFactory.baseCanvas.htmlElement); - expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue); + .prepend(originalFactory.baseCanvas.hostElement); + expect(originalFactory.baseCanvas.isConnected, isTrue); // Create a few overlay canvases - final List overlays = []; + final List overlays = []; for (int i = 0; i < 3; i++) { - final RenderCanvas canvas = originalFactory.getCanvas(); - implicitView.dom.sceneHost.prepend(canvas.htmlElement); + final DisplayCanvas canvas = originalFactory.getCanvas(); + implicitView.dom.sceneHost.prepend(canvas.hostElement); overlays.add(canvas); } expect(originalFactory.debugSurfaceCount, 4); diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index 920b9fa40dc52..0df8e0fb052cc 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -68,7 +68,8 @@ void testMain() { 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 CkSurface hugeSurface = + surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; final DomOffscreenCanvas huge = surface.debugOffscreenCanvas!; expect(huge, same(secondIncrease)); expect(hugeSurface, isNot(same(secondIncreaseSurface))); @@ -106,6 +107,95 @@ void testMain() { // surfaces. }, skip: isFirefox || !Surface.offscreenCanvasSupported); + test('Surface used as DisplayCanvas resizes correctly', () { + final Surface surface = Surface(isDisplayCanvas: true); + + surface.createOrUpdateSurface(const ui.Size(9, 19)); + final DomCanvasElement original = getDisplayCanvas(surface); + ui.Size canvasSize = getCssSize(surface); + + // Expect exact requested dimensions. + expect(original.width, 9); + expect(original.height, 19); + expect(canvasSize.width, 9); + expect(canvasSize.height, 19); + + // Shrinking reuses the existing canvas but translates it so + // Skia renders into the visible area. + surface.createOrUpdateSurface(const ui.Size(5, 15)); + final DomCanvasElement shrunk = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + expect(shrunk.width, 9); + expect(shrunk.height, 19); + expect(canvasSize.width, 9); + expect(canvasSize.height, 19); + + // The first increase will allocate a new surface, but will overallocate + // by 40% to accommodate future increases. + surface.createOrUpdateSurface(const ui.Size(10, 20)); + final DomCanvasElement firstIncrease = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + + expect(firstIncrease, same(original)); + + // Expect overallocated dimensions + expect(firstIncrease.width, 14); + expect(firstIncrease.height, 28); + expect(canvasSize.width, 14); + expect(canvasSize.height, 28); + + // Subsequent increases within 40% reuse the old canvas. + surface.createOrUpdateSurface(const ui.Size(11, 22)); + final DomCanvasElement secondIncrease = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + + expect(secondIncrease, same(firstIncrease)); + expect(secondIncrease.width, 14); + expect(secondIncrease.height, 28); + expect(canvasSize.width, 14); + expect(canvasSize.height, 28); + + // Increases beyond the 40% limit will cause a new allocation. + surface.createOrUpdateSurface(const ui.Size(20, 40)); + final DomCanvasElement huge = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + + expect(huge, same(secondIncrease)); + + // Also over-allocated + expect(huge.width, 28); + expect(huge.height, 56); + expect(canvasSize.width, 28); + expect(canvasSize.height, 56); + + // Shrink again. Reuse the last allocated surface. + surface.createOrUpdateSurface(const ui.Size(5, 15)); + final DomCanvasElement shrunk2 = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + + expect(shrunk2, same(huge)); + expect(shrunk2.width, 28); + expect(shrunk2.height, 56); + expect(canvasSize.width, 28); + expect(canvasSize.height, 56); + + // Doubling the DPR should halve the CSS width, height, and translation of the canvas. + // This tests https://github.com/flutter/flutter/issues/77084 + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); + surface.createOrUpdateSurface(const ui.Size(5, 15)); + final DomCanvasElement dpr2Canvas = getDisplayCanvas(surface); + canvasSize = getCssSize(surface); + + expect(dpr2Canvas, same(huge)); + expect(dpr2Canvas.width, 28); + expect(dpr2Canvas.height, 56); + // Canvas is half the size in logical pixels because device pixel ratio is + // 2.0. + expect(canvasSize.width, 14); + expect(canvasSize.height, 28); + // Skip on wasm since same() doesn't work for JSValues. + }, skip: isWasm); + test( 'Surface creates new context when WebGL context is restored', () async { @@ -130,17 +220,20 @@ void testMain() { 'getExtension', ['WEBGL_lose_context'], ); - js_util.callMethod(loseContextExtension, 'loseContext', const []); + js_util.callMethod( + loseContextExtension, 'loseContext', const []); // Pump a timer to allow the "lose context" event to propagate. await Future.delayed(Duration.zero); // We don't create a new GL context until the context is restored. expect(surface.debugContextLost, isTrue); - final bool isContextLost = js_util.callMethod(ctx, 'isContextLost', const []); + final bool isContextLost = + js_util.callMethod(ctx, 'isContextLost', const []); expect(isContextLost, isTrue); // Emulate WebGL context restoration. - js_util.callMethod(loseContextExtension, 'restoreContext', const []); + js_util.callMethod( + loseContextExtension, 'restoreContext', const []); // Pump a timer to allow the "restore context" event to propagate. await Future.delayed(Duration.zero); @@ -156,46 +249,68 @@ void testMain() { ); // Regression test for https://github.com/flutter/flutter/issues/75286 - test('updates canvas logical size when device-pixel ratio changes', () { - final Surface surface = Surface(); - final CkSurface original = - surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; + test( + 'updates canvas logical size when device-pixel ratio changes', + () { + final Surface surface = Surface(); + final CkSurface original = + surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; - expect(original.width(), 10); - expect(original.height(), 16); - expect(surface.debugOffscreenCanvas!.width, 10); - expect(surface.debugOffscreenCanvas!.height, 16); + expect(original.width(), 10); + expect(original.height(), 16); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); - // Increase device-pixel ratio: this makes CSS pixels bigger, so we need - // fewer of them to cover the browser window. - EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); - final CkSurface highDpr = - 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); - - // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need - // more of them to cover the browser window. - EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(0.5); - final CkSurface lowDpr = - 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); - - // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 - EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); - final CkSurface changeRatioAndSize = - 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); + // Increase device-pixel ratio: this makes CSS pixels bigger, so we need + // fewer of them to cover the browser window. + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); + final CkSurface highDpr = + 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); + + // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need + // more of them to cover the browser window. + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(0.5); + final CkSurface lowDpr = + 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); + + // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); + final CkSurface changeRatioAndSize = + 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, ); }); } + +DomCanvasElement getDisplayCanvas(Surface surface) { + assert(surface.isDisplayCanvas); + return surface.hostElement.children.first as DomCanvasElement; +} + +/// Extracts the CSS style values of 'width' and 'height' and returns them +/// as a [ui.Size]. +ui.Size getCssSize(Surface surface) { + final DomCanvasElement canvas = getDisplayCanvas(surface); + final String cssWidth = canvas.style.width; + final String cssHeight = canvas.style.height; + // CSS width and height should be in the form 'NNNpx'. So cut off the 'px' and + // convert to a number. + final double width = + double.parse(cssWidth.substring(0, cssWidth.length - 2).trim()); + final double height = + double.parse(cssHeight.substring(0, cssHeight.length - 2).trim()); + return ui.Size(width, height); +}