From 73861e29611df33148daa53107ef067393cf5c90 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Mon, 5 Jun 2023 15:23:20 -0700 Subject: [PATCH 01/30] WIP --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 76719d68dcea8..94e689aeae3cf 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -163,18 +163,6 @@ class Surface { 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) { From 1b8cf825507f35c22adc11d6b3f3137b01658474 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 6 Jun 2023 10:27:50 -0700 Subject: [PATCH 02/30] WIP --- .../lib/src/engine/canvaskit/surface.dart | 38 +++++++++---------- .../test/canvaskit/surface_factory_test.dart | 4 +- lib/web_ui/test/canvaskit/surface_test.dart | 38 +++++++++---------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 94e689aeae3cf..45dfb5feba5d2 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -94,7 +94,7 @@ class Surface { final DomElement htmlElement = createDomElement('flt-canvas-container'); /// The underlying `` element used for this surface. - DomCanvasElement? htmlCanvas; + DomOffscreenCanvas? offscreenCanvas; int _pixelWidth = -1; int _pixelHeight = -1; int _sampleCount = -1; @@ -193,8 +193,8 @@ class Surface { final ui.Size newSize = size * 1.4; _surface?.dispose(); _surface = null; - htmlCanvas!.width = newSize.width; - htmlCanvas!.height = newSize.height; + offscreenCanvas!.width = newSize.width; + offscreenCanvas!.height = newSize.height; _currentCanvasPhysicalSize = newSize; _pixelWidth = newSize.width.ceil(); _pixelHeight = newSize.height.ceil(); @@ -237,7 +237,7 @@ class Surface { void _updateLogicalHtmlCanvasSize() { final double logicalWidth = _pixelWidth / window.devicePixelRatio; final double logicalHeight = _pixelHeight / window.devicePixelRatio; - final DomCSSStyleDeclaration style = htmlCanvas!.style; + final DomCSSStyleDeclaration style = offscreenCanvas!.style; style.width = '${logicalWidth}px'; style.height = '${logicalHeight}px'; } @@ -254,7 +254,7 @@ class Surface { final int surfaceHeight = _currentSurfaceSize!.height.ceil(); final double offset = (_pixelHeight - surfaceHeight) / window.devicePixelRatio; - htmlCanvas!.style.transform = 'translate(0, -${offset}px)'; + offscreenCanvas!.style.transform = 'translate(0, -${offset}px)'; } JSVoid _contextRestoredListener(DomEvent event) { @@ -270,7 +270,7 @@ class Surface { } JSVoid _contextLostListener(DomEvent event) { - assert(event.target == htmlCanvas, + assert(event.target == offscreenCanvas, 'Received a context lost event for a disposed canvas'); final SurfaceFactory factory = SurfaceFactory.instance; _contextLost = true; @@ -287,18 +287,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 (this.htmlCanvas != null) { - this.htmlCanvas!.removeEventListener( + if (this.offscreenCanvas != null) { + this.offscreenCanvas!.removeEventListener( 'webglcontextrestored', _cachedContextRestoredListener, false, ); - this.htmlCanvas!.removeEventListener( + this.offscreenCanvas!.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false, ); - this.htmlCanvas!.remove(); + this.offscreenCanvas!.remove(); _cachedContextRestoredListener = null; _cachedContextLostListener = null; } @@ -311,7 +311,7 @@ class Surface { width: _pixelWidth, height: _pixelHeight, ); - this.htmlCanvas = htmlCanvas; + this.offscreenCanvas = 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 @@ -380,22 +380,22 @@ class Surface { } void _initWebglParams() { - final WebGLContext gl = htmlCanvas!.getGlContext(webGLVersion); + final WebGLContext gl = offscreenCanvas!.getGlContext(webGLVersion); _sampleCount = gl.getParameter(gl.samples); _stencilBits = gl.getParameter(gl.stencilBits); } CkSurface _createNewSurface(ui.Size size) { - assert(htmlCanvas != null); + assert(offscreenCanvas != null); if (webGLVersion == -1) { return _makeSoftwareCanvasSurface( - htmlCanvas!, 'WebGL support not detected'); + offscreenCanvas!, 'WebGL support not detected'); } else if (configuration.canvasKitForceCpuOnly) { return _makeSoftwareCanvasSurface( - htmlCanvas!, 'CPU rendering forced by application'); + offscreenCanvas!, 'CPU rendering forced by application'); } else if (_glContext == 0) { return _makeSoftwareCanvasSurface( - htmlCanvas!, 'Failed to initialize WebGL context'); + offscreenCanvas!, 'Failed to initialize WebGL context'); } else { final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, @@ -408,7 +408,7 @@ class Surface { if (skSurface == null) { return _makeSoftwareCanvasSurface( - htmlCanvas!, 'Failed to initialize WebGL surface'); + offscreenCanvas!, 'Failed to initialize WebGL surface'); } return CkSurface(skSurface, _glContext); @@ -435,9 +435,9 @@ class Surface { } void dispose() { - htmlCanvas?.removeEventListener( + offscreenCanvas?.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false); - htmlCanvas?.removeEventListener( + offscreenCanvas?.removeEventListener( 'webglcontextrestored', _cachedContextRestoredListener, false); _cachedContextLostListener = null; _cachedContextRestoredListener = null; diff --git a/lib/web_ui/test/canvaskit/surface_factory_test.dart b/lib/web_ui/test/canvaskit/surface_factory_test.dart index 05db21472386c..73aff4f559017 100644 --- a/lib/web_ui/test/canvaskit/surface_factory_test.dart +++ b/lib/web_ui/test/canvaskit/surface_factory_test.dart @@ -72,7 +72,7 @@ void testMain() { test('hot restart', () { void expectDisposed(Surface surface) { - expect(surface.htmlCanvas!.isConnected, isFalse); + expect(surface.offscreenCanvas!.isConnected, isFalse); } final SurfaceFactory originalFactory = SurfaceFactory.instance; @@ -81,7 +81,7 @@ void testMain() { // 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); + expect(originalFactory.baseSurface.offscreenCanvas!.isConnected, isTrue); // Create a few overlay surfaces final List overlays = []; diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index e31ca43bf8079..77b20f4dd8203 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -26,7 +26,7 @@ void testMain() { final Surface? surface = SurfaceFactory.instance.getSurface(); final CkSurface originalSurface = surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; - final DomCanvasElement original = surface.htmlCanvas!; + final DomCanvasElement original = surface.offscreenCanvas!; // Expect exact requested dimensions. expect(original.width, 9); @@ -41,7 +41,7 @@ void testMain() { // Skia renders into the visible area. final CkSurface shrunkSurface = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk = surface.htmlCanvas!; + final DomCanvasElement shrunk = surface.offscreenCanvas!; expect(shrunk, same(original)); expect(shrunk.style.width, '9px'); expect(shrunk.style.height, '19px'); @@ -54,7 +54,7 @@ void testMain() { // by 40% to accommodate future increases. final CkSurface firstIncreaseSurface = surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomCanvasElement firstIncrease = surface.htmlCanvas!; + final DomCanvasElement firstIncrease = surface.offscreenCanvas!; expect(firstIncrease, same(original)); expect(firstIncreaseSurface, isNot(same(shrunkSurface))); @@ -70,7 +70,7 @@ void testMain() { // Subsequent increases within 40% reuse the old canvas. final CkSurface secondIncreaseSurface = surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; - final DomCanvasElement secondIncrease = surface.htmlCanvas!; + final DomCanvasElement secondIncrease = surface.offscreenCanvas!; expect(secondIncrease, same(firstIncrease)); expect(secondIncrease.style.transform, _isTranslate('0', '-6')); expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); @@ -79,7 +79,7 @@ void testMain() { // Increases beyond the 40% limit will cause a new allocation. final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; - final DomCanvasElement huge = surface.htmlCanvas!; + final DomCanvasElement huge = surface.offscreenCanvas!; expect(huge, same(secondIncrease)); expect(hugeSurface, isNot(same(secondIncreaseSurface))); @@ -95,7 +95,7 @@ void testMain() { // Shrink again. Reuse the last allocated surface. final CkSurface shrunkSurface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk2 = surface.htmlCanvas!; + final DomCanvasElement shrunk2 = surface.offscreenCanvas!; expect(shrunk2, same(huge)); expect(shrunk2.style.width, '28px'); expect(shrunk2.style.height, '56px'); @@ -109,7 +109,7 @@ void testMain() { window.debugOverrideDevicePixelRatio(2.0); final CkSurface dpr2Surface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement dpr2Canvas = surface.htmlCanvas!; + final DomCanvasElement dpr2Canvas = surface.offscreenCanvas!; expect(dpr2Canvas, same(huge)); expect(dpr2Canvas.style.width, '14px'); expect(dpr2Canvas.style.height, '28px'); @@ -183,9 +183,9 @@ void testMain() { expect(original.width(), 10); expect(original.height(), 16); - expect(surface.htmlCanvas!.style.width, '10px'); - expect(surface.htmlCanvas!.style.height, '16px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.style.width, '10px'); + expect(surface.offscreenCanvas!.style.height, '16px'); + expect(surface.offscreenCanvas!.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. @@ -194,9 +194,9 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(highDpr.width(), 10); expect(highDpr.height(), 16); - expect(surface.htmlCanvas!.style.width, '5px'); - expect(surface.htmlCanvas!.style.height, '8px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.style.width, '5px'); + expect(surface.offscreenCanvas!.style.height, '8px'); + expect(surface.offscreenCanvas!.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. @@ -205,9 +205,9 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(lowDpr.width(), 10); expect(lowDpr.height(), 16); - expect(surface.htmlCanvas!.style.width, '20px'); - expect(surface.htmlCanvas!.style.height, '32px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.style.width, '20px'); + expect(surface.offscreenCanvas!.style.height, '32px'); + expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); @@ -215,9 +215,9 @@ void testMain() { surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; expect(changeRatioAndSize.width(), 10); expect(changeRatioAndSize.height(), 16); - expect(surface.htmlCanvas!.style.width, '5px'); - expect(surface.htmlCanvas!.style.height, '8px'); - expect(surface.htmlCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.style.width, '5px'); + expect(surface.offscreenCanvas!.style.height, '8px'); + expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); }); }); } From 513e0ed6cd4f88ce98d1e3ba3df47d64e8b022da Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 7 Jun 2023 12:42:27 -0700 Subject: [PATCH 03/30] WIP --- lib/web_ui/lib/src/engine.dart | 3 +- .../src/engine/canvaskit/embedded_views.dart | 73 ++------ .../lib/src/engine/canvaskit/picture.dart | 2 +- .../lib/src/engine/canvaskit/rasterizer.dart | 6 +- .../src/engine/canvaskit/render_canvas.dart | 149 ++++++++++++++++ .../canvaskit/render_canvas_factory.dart | 143 +++++++++++++++ .../lib/src/engine/canvaskit/surface.dart | 7 +- .../src/engine/canvaskit/surface_factory.dart | 167 ------------------ .../test/canvaskit/canvas_golden_test.dart | 2 +- lib/web_ui/test/canvaskit/common.dart | 2 +- .../test/canvaskit/embedded_views_test.dart | 12 +- .../canvaskit/render_canvas_factory_test.dart | 96 ++++++++++ .../test/canvaskit/surface_factory_test.dart | 103 ----------- lib/web_ui/test/canvaskit/surface_test.dart | 4 +- 14 files changed, 421 insertions(+), 348 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart create mode 100644 lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart delete mode 100644 lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart create mode 100644 lib/web_ui/test/canvaskit/render_canvas_factory_test.dart delete mode 100644 lib/web_ui/test/canvaskit/surface_factory_test.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index e5c92bfe21517..31c80e7da955e 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -44,10 +44,11 @@ export 'engine/canvaskit/picture.dart'; export 'engine/canvaskit/picture_recorder.dart'; export 'engine/canvaskit/raster_cache.dart'; export 'engine/canvaskit/rasterizer.dart'; +export 'engine/canvaskit/render_canvas.dart'; +export 'engine/canvaskit/render_canvas_factory.dart'; export 'engine/canvaskit/renderer.dart'; export 'engine/canvaskit/shader.dart'; export 'engine/canvaskit/surface.dart'; -export 'engine/canvaskit/surface_factory.dart'; export 'engine/canvaskit/text.dart'; export 'engine/canvaskit/text_fragmenter.dart'; export 'engine/canvaskit/util.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 70a1a3cb1d7d0..a32e725958eda 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -17,9 +17,10 @@ import 'canvas.dart'; import 'embedded_views_diff.dart'; import 'path.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,42 +31,6 @@ class HtmlViewEmbedder { DomElement get skiaSceneHost => CanvasKitRenderer.instance.sceneHost!; - /// Force the view embedder to disable overlays. - /// - /// This should never be used outside of tests. - static set debugDisableOverlays(bool disable) { - // Short circuit if the value is the same as what we already have. - if (disable == _debugOverlaysDisabled) { - return; - } - _debugOverlaysDisabled = disable; - final SurfaceFactory? instance = SurfaceFactory.debugUninitializedInstance; - if (instance != null) { - instance.releaseSurfaces(); - instance.removeSurfacesFromDom(); - instance.debugClear(); - } - if (disable) { - // If we are disabling overlays then get the current [SurfaceFactory] - // instance, clear it, and overwrite it with a new instance with only - // one surface for the base surface. - SurfaceFactory.debugSetInstance(SurfaceFactory(1)); - } else { - // If we are re-enabling overlays then replace the current - // [SurfaceFactory]instance with one with - // [configuration.canvasKitMaximumSurfaces] overlays. - SurfaceFactory.debugSetInstance( - SurfaceFactory(configuration.canvasKitMaximumSurfaces)); - } - } - - static bool _debugOverlaysDisabled = false; - - /// Whether or not we have issues a warning to the user about having too many - /// surfaces on screen at once. This is so we only warn once, instead of every - /// frame. - bool _warnedAboutTooManySurfaces = false; - /// The context for the current frame. EmbedderFrameContext _context = EmbedderFrameContext(); @@ -85,10 +50,8 @@ class HtmlViewEmbedder { /// * The number of clipping elements used last time the view was composited. final Map _viewClipChains = {}; - /// Surfaces used to draw on top of platform views, keyed by platform view ID. - /// - /// These surfaces are cached in the [OverlayCache] and reused. - final Map _overlays = {}; + /// Canvases used to draw on top of platform views, keyed by platform view ID. + final Map _overlays = {}; /// The views that need to be recomposited into the scene on the next frame. final Set _viewsToRecomposite = {}; @@ -123,20 +86,10 @@ class HtmlViewEmbedder { } void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { - final bool hasAvailableOverlay = - _context.pictureRecordersCreatedDuringPreroll.length < - SurfaceFactory.instance.maximumOverlays; - if (!hasAvailableOverlay && !_warnedAboutTooManySurfaces) { - _warnedAboutTooManySurfaces = true; - printWarning('Flutter was unable to create enough overlay surfaces. ' - 'This is usually caused by too many platform views being ' - 'displayed at once. ' - 'You may experience incorrect rendering.'); - } // We need an overlay for each visible platform view. Invisible platform // views will be grouped with (at most) one visible platform view later. final bool needNewOverlay = platformViewManager.isVisible(viewId); - if (needNewOverlay && hasAvailableOverlay) { + if (needNewOverlay) { final CkPictureRecorder pictureRecorder = CkPictureRecorder(); pictureRecorder.beginRecording(ui.Offset.zero & _frameSize); pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); @@ -513,7 +466,7 @@ class HtmlViewEmbedder { } } } else { - SurfaceFactory.instance.removeSurfacesFromDom(); + RenderCanvasFactory.instance.removeSurfacesFromDom(); for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; @@ -567,8 +520,8 @@ class HtmlViewEmbedder { void _releaseOverlay(int viewId) { if (_overlays[viewId] != null) { - final Surface overlay = _overlays[viewId]!; - SurfaceFactory.instance.releaseSurface(overlay); + final RenderCanvas overlay = _overlays[viewId]!; + RenderCanvasFactory.instance.releaseCanvas(overlay); _overlays.remove(viewId); } } @@ -607,15 +560,15 @@ class HtmlViewEmbedder { 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) { + if (viewsNeedingOverlays.length > RenderCanvasFactory.instance.maximumOverlays) { assert(viewsNeedingOverlays.length == - SurfaceFactory.instance.maximumOverlays + 1); + RenderCanvasFactory.instance.maximumOverlays + 1); viewsNeedingOverlays.removeLast(); } if (diffResult == null) { // Everything is going to be explicitly recomposited anyway. Release all // the surfaces and assign an overlay to all the surfaces needing one. - SurfaceFactory.instance.releaseSurfaces(); + RenderCanvasFactory.instance.releaseCanvases(); _overlays.clear(); viewsNeedingOverlays.forEach(_initializeOverlay); } else { @@ -645,7 +598,7 @@ 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; + final int maxOverlays = RenderCanvasFactory.instance.maximumOverlays; if (maxOverlays == 0) { return const []; } @@ -695,7 +648,7 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final Surface overlay = SurfaceFactory.instance.getSurface()!; + final Surface overlay = RenderCanvasFactory.instance.getCanvas()!; overlay.createOrUpdateSurface(_frameSize); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 374c24655d721..6dff747c5dce0 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -95,7 +95,7 @@ class CkPicture implements ui.Picture { CkImage toImageSync(int width, int height) { assert(debugCheckNotDisposed('Cannot convert picture to image.')); - final Surface surface = SurfaceFactory.instance.pictureToImageSurface; + final Surface surface = RenderCanvasFactory.instance.pictureToImageSurface; final CkSurface ckSurface = surface.createOrUpdateSurface(ui.Size(width.toDouble(), height.toDouble())); final CkCanvas ckCanvas = ckSurface.getCanvas(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index b8b420bb829a6..3fdb038e759f3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -18,7 +18,7 @@ class Rasterizer { final List _postFrameCallbacks = []; void setSkiaResourceCacheMaxBytes(int bytes) => - SurfaceFactory.instance.baseSurface.setSkiaResourceCacheMaxBytes(bytes); + RenderCanvasFactory.instance.baseCanvas.setSkiaResourceCacheMaxBytes(bytes); /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -30,14 +30,14 @@ class Rasterizer { } final SurfaceFrame frame = - SurfaceFactory.instance.baseSurface.acquireFrame(layerTree.frameSize); + RenderCanvasFactory.instance.baseCanvas.acquireFrame(layerTree.frameSize); HtmlViewEmbedder.instance.frameSize = layerTree.frameSize; final CkCanvas canvas = frame.skiaCanvas; final Frame compositorFrame = context.acquireFrame(canvas, HtmlViewEmbedder.instance); compositorFrame.raster(layerTree, ignoreRasterCache: true); - SurfaceFactory.instance.baseSurface.addToScene(); + RenderCanvasFactory.instance.baseCanvas.addToScene(); frame.submit(); HtmlViewEmbedder.instance.submitFrame(); } finally { diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart new file mode 100644 index 0000000000000..cc451acafd682 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -0,0 +1,149 @@ +// 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 '../browser_detection.dart'; +import '../configuration.dart'; +import '../dom.dart'; +import '../platform_dispatcher.dart'; +import '../util.dart'; +import '../window.dart'; +import 'canvas.dart'; +import 'canvaskit_api.dart'; +import 'renderer.dart'; +import 'surface_factory.dart'; +import 'util.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(); + + /// 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. + DomCanvasElement? canvasElement; + int _pixelWidth = -1; + int _pixelHeight = -1; + + ui.Size? _currentCanvasPhysicalSize; + ui.Size? _currentSurfaceSize; + double _currentDevicePixelRatio = -1; + + bool _addedToScene = false; + void addToScene() { + if (!_addedToScene) { + CanvasKitRenderer.instance.sceneHost!.prepend(htmlElement); + } + _addedToScene = true; + } + + /// 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'; + } + + /// Creates a and SkSurface for the given [size]. + CkSurface createOrUpdateSurface(ui.Size size) { + if (size.isEmpty) { + throw CanvasKitError('Cannot create surfaces of empty size.'); + } + + // Check if the window is the same size as before, and if so, don't allocate + // a new canvas as the previous canvas is big enough to fit everything. + final ui.Size? previousSurfaceSize = _currentSurfaceSize; + 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!; + } + + final ui.Size? previousCanvasSize = _currentCanvasPhysicalSize; + // Initialize a new, larger, canvas. If the size is growing, then make the + // new canvas larger than required to avoid many canvas creations. + if (previousCanvasSize != null && + (size.width > previousCanvasSize.width || + size.height > previousCanvasSize.height)) { + final ui.Size newSize = size * 1.4; + _surface?.dispose(); + _surface = null; + offscreenCanvas!.width = newSize.width; + offscreenCanvas!.height = newSize.height; + _currentCanvasPhysicalSize = newSize; + _pixelWidth = newSize.width.ceil(); + _pixelHeight = newSize.height.ceil(); + _updateLogicalHtmlCanvasSize(); + } + + // This is the first frame we have rendered with this canvas. + if (_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!; + } + + void dispose() { + htmlElement.remove(); + } +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart new file mode 100644 index 0000000000000..5116abfe5d06c --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart @@ -0,0 +1,143 @@ +// 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'; +import 'render_canvas.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 45dfb5feba5d2..12ff705e45190 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -14,8 +14,8 @@ import '../util.dart'; import '../window.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; +import 'render_canvas_factory.dart'; import 'renderer.dart'; -import 'surface_factory.dart'; import 'util.dart'; // Only supported in profile/release mode. Allows Flutter to use MSAA but @@ -35,6 +35,7 @@ class SurfaceFrame { /// Submit this frame to be drawn. bool submit() { + if (_submitted) { return false; } @@ -159,7 +160,7 @@ class Surface { } // TODO(jonahwilliams): this is somewhat wasteful. We should probably // eagerly setup this surface instead of delaying until the first frame? - // Or at least cache the estimated window size. + // Or at least cache the estimated window sizeThis is the first frame we have rendered with this canvas. createOrUpdateSurface(size); } @@ -272,7 +273,7 @@ class Surface { JSVoid _contextLostListener(DomEvent event) { assert(event.target == offscreenCanvas, 'Received a context lost event for a disposed canvas'); - final SurfaceFactory factory = SurfaceFactory.instance; + final RenderCanvasFactory factory = RenderCanvasFactory.instance; _contextLost = true; if (factory.isLive(this)) { _forceNewContext = true; diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart b/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart deleted file mode 100644 index ee5b001dd8e04..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -import 'dart:math' as math show max; - -import 'package:meta/meta.dart'; - -import '../../engine.dart'; - -/// Caches surfaces used to overlay platform views. -class SurfaceFactory { - SurfaceFactory(int maximumSurfaces) - : maximumSurfaces = math.max(maximumSurfaces, 1) { - assert(() { - if (maximumSurfaces < 1) { - printWarning('Attempted to create a $SurfaceFactory with ' - '$maximumSurfaces maximum surfaces. At least 1 surface is required ' - 'for rendering.'); - } - registerHotRestartListener(debugClear); - return true; - }()); - } - - /// The lazy-initialized singleton surface factory. - /// - /// [debugClear] causes this singleton to be reinitialized. - static SurfaceFactory get instance => - _instance ??= SurfaceFactory(configuration.canvasKitMaximumSurfaces); - - /// Returns the raw (potentially uninitialized) value of the singleton. - /// - /// Useful in tests for checking the lifecycle of this class. - static SurfaceFactory? get debugUninitializedInstance => _instance; - - // Override the current instance with a new one. - // - // This should only be used in tests. - static void debugSetInstance(SurfaceFactory newInstance) { - _instance = newInstance; - } - - static SurfaceFactory? _instance; - - /// The base surface to paint on. This is the default surface which will be - /// painted to. If there are no platform views, then this surface will receive - /// all painting commands. - final Surface baseSurface = Surface(); - - /// The maximum number of surfaces which can be live at once. - final int maximumSurfaces; - - /// A surface used specifically for `Picture.toImage` when software rendering - /// is supported. - late final Surface pictureToImageSurface = Surface(); - - /// The maximum number of assignable overlays. - /// - /// This is just `maximumSurfaces - 1` (the maximum number of surfaces minus - /// the required base surface). - int get maximumOverlays => maximumSurfaces - 1; - - /// Surfaces created by this factory which are currently in use. - final List _liveSurfaces = []; - - /// Surfaces created by this factory which are no longer in use. These can be - /// reused. - final List _cache = []; - - /// The number of surfaces which have been created by this factory. - int get _surfaceCount => _liveSurfaces.length + _cache.length + 1; - - /// The number of available overlay surfaces. - /// - /// This does not include the base surface. - int get numAvailableOverlays => maximumOverlays - _liveSurfaces.length; - - /// The number of surfaces created by this factory. Used for testing. - @visibleForTesting - int get debugSurfaceCount => _surfaceCount; - - /// Returns the number of cached surfaces. - /// - /// Useful in tests. - int get debugCacheSize => _cache.length; - - /// Gets an overlay surface from the cache or creates a new one if it wouldn't - /// exceed the maximum. If there are no available surfaces, returns `null`. - Surface? getSurface() { - if (_cache.isNotEmpty) { - final Surface surface = _cache.removeLast(); - _liveSurfaces.add(surface); - return surface; - } else if (debugSurfaceCount < maximumSurfaces) { - final Surface surface = Surface(); - _liveSurfaces.add(surface); - return surface; - } else { - return null; - } - } - - /// Releases all surfaces so they can be reused in the next frame. - /// - /// If a released surface is in the DOM, it is not removed. This allows the - /// engine to release the surfaces at the end of the frame so they are ready - /// to be used in the next frame, but still used for painting in the current - /// frame. - void releaseSurfaces() { - _cache.addAll(_liveSurfaces); - _liveSurfaces.clear(); - } - - /// Removes all surfaces except the base surface from the DOM. - /// - /// This is called at the beginning of the frame to prepare for painting into - /// the new surfaces. - void removeSurfacesFromDom() { - _cache.forEach(_removeFromDom); - } - - // Removes [surface] from the DOM. - void _removeFromDom(Surface surface) { - surface.htmlElement.remove(); - } - - /// Signals that a surface is no longer being used. It can be reused. - void releaseSurface(Surface surface) { - assert(surface != baseSurface, 'Attempting to release the base surface'); - assert( - _liveSurfaces.contains(surface), - 'Attempting to release a Surface which ' - 'was not created by this factory'); - surface.htmlElement.remove(); - _liveSurfaces.remove(surface); - _cache.add(surface); - } - - /// Returns [true] if [surface] is currently being used to paint content. - /// - /// The base surface always counts as live. - /// - /// If a surface is not live, then it must be in the cache and ready to be - /// reused. - bool isLive(Surface surface) { - if (surface == baseSurface || - _liveSurfaces.contains(surface)) { - return true; - } - assert(_cache.contains(surface)); - return false; - } - - /// Dispose all surfaces created by this factory. Used in tests. - void debugClear() { - for (final Surface surface in _cache) { - surface.dispose(); - } - for (final Surface surface in _liveSurfaces) { - surface.dispose(); - } - baseSurface.dispose(); - _liveSurfaces.clear(); - _cache.clear(); - _instance = null; - } -} diff --git a/lib/web_ui/test/canvaskit/canvas_golden_test.dart b/lib/web_ui/test/canvaskit/canvas_golden_test.dart index 361e823b6e37e..46a436db87726 100644 --- a/lib/web_ui/test/canvaskit/canvas_golden_test.dart +++ b/lib/web_ui/test/canvaskit/canvas_golden_test.dart @@ -163,7 +163,7 @@ void testMain() { // Regression test for https://github.com/flutter/flutter/issues/121758 test('resources used in temporary surfaces for Image.toByteData can cross to rendering overlays', () async { final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - SurfaceFactory.instance.debugClear(); + RenderCanvasFactory.instance.debugClear(); ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', diff --git a/lib/web_ui/test/canvaskit/common.dart b/lib/web_ui/test/canvaskit/common.dart index a8a2b04ce9469..bf0fd59602968 100644 --- a/lib/web_ui/test/canvaskit/common.dart +++ b/lib/web_ui/test/canvaskit/common.dart @@ -24,7 +24,7 @@ void setUpCanvasKitTest() { tearDown(() { HtmlViewEmbedder.instance.debugClear(); - SurfaceFactory.instance.debugClear(); + RenderCanvasFactory.instance.debugClear(); }); setUp(() => diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index a5d15dd055517..c6fcfb3d85dfa 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -292,7 +292,7 @@ void testMain() { }); test('renders overlays on top of platform views', () async { - expect(SurfaceFactory.instance.debugCacheSize, 0); + expect(RenderCanvasFactory.instance.debugCacheSize, 0); expect(configuration.canvasKitMaximumSurfaces, 8); final CkPicture testPicture = paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) { @@ -477,7 +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); + expect(RenderCanvasFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -504,7 +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); + expect(RenderCanvasFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -813,10 +813,10 @@ void testMain() { }) as JsFlutterConfiguration); debugSetConfiguration(config); - SurfaceFactory.instance.debugClear(); + RenderCanvasFactory.instance.debugClear(); - expect(SurfaceFactory.instance.maximumSurfaces, 2); - expect(SurfaceFactory.instance.maximumOverlays, 1); + expect(RenderCanvasFactory.instance.maximumSurfaces, 2); + expect(RenderCanvasFactory.instance.maximumOverlays, 1); ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart new file mode 100644 index 0000000000000..1338b2559ce1e --- /dev/null +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -0,0 +1,96 @@ +// 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('$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 + originalFactory.baseCanvas.acquireFrame(const ui.Size(10, 10)); + originalFactory.baseCanvas.addToScene(); + expect(originalFactory.baseCanvas.offscreenCanvas!.isConnected, isTrue); + + // Create a few overlay canvases + final List overlays = []; + for (int i = 0; i < 3; i++) { + overlays.add(originalFactory.getCanvas()! + ..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(RenderCanvasFactory.debugUninitializedInstance, isNull); + expectDisposed(originalFactory.baseCanvas); + overlays.forEach(expectDisposed); + expect(originalFactory.debugSurfaceCount, 1); + }); + }); +} diff --git a/lib/web_ui/test/canvaskit/surface_factory_test.dart b/lib/web_ui/test/canvaskit/surface_factory_test.dart deleted file mode 100644 index 73aff4f559017..0000000000000 --- a/lib/web_ui/test/canvaskit/surface_factory_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; -import 'package:ui/ui.dart' as ui; - -import 'common.dart'; - -const MethodCodec codec = StandardMethodCodec(); - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - group('$SurfaceFactory', () { - setUpCanvasKitTest(); - - test('cannot be created with size less than 1', () { - expect(SurfaceFactory(-1).maximumSurfaces, 1); - expect(SurfaceFactory(0).maximumSurfaces, 1); - expect(SurfaceFactory(1).maximumSurfaces, 1); - expect(SurfaceFactory(2).maximumSurfaces, 2); - }); - - test('getSurface', () { - final SurfaceFactory factory = SurfaceFactory(3); - expect(factory.baseSurface, isNotNull); - - expect(factory.debugSurfaceCount, equals(1)); - - // Get a surface from the factory, it should be unique. - final Surface? newSurface = factory.getSurface(); - expect(newSurface, isNot(equals(factory.baseSurface))); - - expect(factory.debugSurfaceCount, equals(2)); - - // Get another surface from the factory. Now we are at maximum capacity. - final Surface? anotherSurface = factory.getSurface(); - expect(anotherSurface, isNot(equals(factory.baseSurface))); - - expect(factory.debugSurfaceCount, equals(3)); - }); - - test('releaseSurface', () { - final SurfaceFactory factory = SurfaceFactory(3); - - // Create a new surface and immediately release it. - final Surface? surface = factory.getSurface(); - factory.releaseSurface(surface!); - - // If we create a new surface, it should be the same as the one we - // just created. - final Surface? newSurface = factory.getSurface(); - expect(newSurface, equals(surface)); - }); - - test('isLive', () { - final SurfaceFactory factory = SurfaceFactory(3); - - expect(factory.isLive(factory.baseSurface), isTrue); - - final Surface? surface = factory.getSurface(); - expect(factory.isLive(surface!), isTrue); - - factory.releaseSurface(surface); - expect(factory.isLive(surface), isFalse); - }); - - test('hot restart', () { - void expectDisposed(Surface surface) { - expect(surface.offscreenCanvas!.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.offscreenCanvas!.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 77b20f4dd8203..22f49fee75001 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -23,7 +23,7 @@ void testMain() { }); test('Surface allocates canvases efficiently', () { - final Surface? surface = SurfaceFactory.instance.getSurface(); + final Surface? surface = RenderCanvasFactory.instance.getCanvas(); final CkSurface originalSurface = surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; final DomCanvasElement original = surface.offscreenCanvas!; @@ -128,7 +128,7 @@ void testMain() { test( 'Surface creates new context when WebGL context is restored', () async { - final Surface? surface = SurfaceFactory.instance.getSurface(); + final Surface? surface = RenderCanvasFactory.instance.getCanvas(); expect(surface!.debugForceNewContext, isTrue); final CkSurface before = surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; From 961db5515b2a79cb0f33a284155bd470f09bbb93 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 8 Jun 2023 11:35:54 -0700 Subject: [PATCH 04/30] WIP --- .../src/engine/canvaskit/canvaskit_api.dart | 10 + .../src/engine/canvaskit/embedded_views.dart | 47 ++-- .../lib/src/engine/canvaskit/picture.dart | 2 +- .../lib/src/engine/canvaskit/rasterizer.dart | 25 +- .../src/engine/canvaskit/render_canvas.dart | 119 +++++---- .../lib/src/engine/canvaskit/surface.dart | 87 +------ lib/web_ui/lib/src/engine/dom.dart | 28 ++ .../test/canvaskit/embedded_views_test.dart | 66 ----- .../canvaskit/render_canvas_factory_test.dart | 8 +- .../test/canvaskit/render_canvas_test.dart | 239 ++++++++++++++++++ lib/web_ui/test/canvaskit/surface_test.dart | 4 +- 11 files changed, 401 insertions(+), 234 deletions(-) create mode 100644 lib/web_ui/test/canvaskit/render_canvas_test.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 4aa608f4e23c9..e8a317f7ddca0 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -161,6 +161,13 @@ extension CanvasKitExtension on CanvasKit { DomCanvasElement canvas, SkWebGLContextOptions options) => _GetWebGLContext(canvas, options).toDart; + @JS('GetWebGLContext') + external JSNumber _GetOffscreenWebGLContext( + DomOffscreenCanvas canvas, SkWebGLContextOptions options); + double GetOffscreenWebGLContext( + DomOffscreenCanvas canvas, SkWebGLContextOptions options) => + _GetOffscreenWebGLContext(canvas, options).toDart; + @JS('MakeGrContext') external SkGrContext _MakeGrContext(JSNumber glContext); SkGrContext MakeGrContext(double glContext) => @@ -199,6 +206,9 @@ extension CanvasKitExtension on CanvasKit { external SkSurface MakeSWCanvasSurface(DomCanvasElement canvas); + @JS('MakeSWCanvasSurface') + external SkSurface MakeOffscreenSWCanvasSurface(DomOffscreenCanvas canvas); + /// Creates an image from decoded pixels represented as a list of bytes. /// /// The pixel data must be encoded according to the image info in [info]. diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index a32e725958eda..2e67df7535a17 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -5,7 +5,6 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show platformViewManager; -import '../configuration.dart'; import '../dom.dart'; import '../html/path_to_svg_clip.dart'; import '../platform_views/slots.dart'; @@ -370,16 +369,25 @@ class HtmlViewEmbedder { ); int pictureRecorderIndex = 0; + // Prime the SkSurface for rendering. + final SurfaceFrame frame = CanvasKitRenderer + .instance.rasterizer.offscreenSurface + .acquireFrame(_frameSize); + for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; if (_overlays[viewId] != null) { - final SurfaceFrame frame = _overlays[viewId]!.acquireFrame(_frameSize); + // Render the picture to the canvas + _overlays[viewId]!.ensureSize(_frameSize); final CkCanvas canvas = frame.skiaCanvas; + canvas.clear(const ui.Color(0x00000000)); canvas.drawPicture( _context.pictureRecorders[pictureRecorderIndex].endRecording(), ); - pictureRecorderIndex++; frame.submit(); + final DomImageBitmap bitmap = CanvasKitRenderer.instance.rasterizer.offscreenSurface.offscreenCanvas!.transferToImageBitmap(); + _overlays[viewId]!.renderContext!.transferFromImageBitmap(bitmap); + pictureRecorderIndex++; } } for (final CkPictureRecorder recorder @@ -433,7 +441,7 @@ class HtmlViewEmbedder { if (diffResult.addToBeginning) { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.insertBefore(platformViewRoot, elementToInsertBefore); - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.insertBefore( overlay.htmlElement, elementToInsertBefore); @@ -441,7 +449,7 @@ class HtmlViewEmbedder { } else { final DomElement platformViewRoot = _viewClipChains[viewId]!.root; skiaSceneHost.append(platformViewRoot); - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); } @@ -484,7 +492,7 @@ class HtmlViewEmbedder { } final DomElement platformViewRoot = _viewClipChains[viewId]!.root; - final Surface? overlay = _overlays[viewId]; + final RenderCanvas? overlay = _overlays[viewId]; skiaSceneHost.append(platformViewRoot); if (overlay != null) { skiaSceneHost.append(overlay.htmlElement); @@ -558,13 +566,6 @@ 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 > RenderCanvasFactory.instance.maximumOverlays) { - assert(viewsNeedingOverlays.length == - RenderCanvasFactory.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. @@ -598,10 +599,6 @@ 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 = RenderCanvasFactory.instance.maximumOverlays; - if (maxOverlays == 0) { - return const []; - } final List result = []; OverlayGroup currentGroup = OverlayGroup([]); @@ -623,17 +620,7 @@ class HtmlViewEmbedder { // We only care about groups that have one visible view. result.add(currentGroup); } - // If there are overlays still available. - if (result.length < maxOverlays) { - // Create a new group, starting with `view`. - currentGroup = OverlayGroup([view], visible: true); - } else { - // Add the rest of the views to a final group that will be rendered - // on top of the scene. - currentGroup = OverlayGroup(views.sublist(i), visible: true); - // And break out of the loop! - break; - } + currentGroup = OverlayGroup([view], visible: true); } } } @@ -648,8 +635,8 @@ class HtmlViewEmbedder { assert(!_overlays.containsKey(viewId)); // Try reusing a cached overlay created for another platform view. - final Surface overlay = RenderCanvasFactory.instance.getCanvas()!; - overlay.createOrUpdateSurface(_frameSize); + final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas(); + overlay.ensureSize(_frameSize); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index 6dff747c5dce0..6d82e494a0547 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -10,8 +10,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 ui.Picture { diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 3fdb038e759f3..01c1c403c9671 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -3,22 +3,27 @@ // 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 'render_canvas_factory.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(); + void setSkiaResourceCacheMaxBytes(int bytes) => - RenderCanvasFactory.instance.baseCanvas.setSkiaResourceCacheMaxBytes(bytes); + offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -29,16 +34,24 @@ class Rasterizer { return; } - final SurfaceFrame frame = - RenderCanvasFactory.instance.baseCanvas.acquireFrame(layerTree.frameSize); + SurfaceFrame frame = offscreenSurface.acquireFrame(layerTree.frameSize); + RenderCanvasFactory.instance.baseCanvas.ensureSize(layerTree.frameSize); HtmlViewEmbedder.instance.frameSize = layerTree.frameSize; - final CkCanvas canvas = frame.skiaCanvas; + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & layerTree.frameSize); + pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); final Frame compositorFrame = - context.acquireFrame(canvas, HtmlViewEmbedder.instance); + context.acquireFrame(pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance); compositorFrame.raster(layerTree, ignoreRasterCache: true); RenderCanvasFactory.instance.baseCanvas.addToScene(); + + final CkCanvas canvas = frame.skiaCanvas; + canvas.clear(const ui.Color(0x00000000)); + canvas.drawPicture(pictureRecorder.endRecording()); frame.submit(); + final DomImageBitmap bitmap = CanvasKitRenderer.instance.rasterizer.offscreenSurface.offscreenCanvas!.transferToImageBitmap(); + RenderCanvasFactory.instance.baseCanvas.renderContext!.transferFromImageBitmap(bitmap); 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 index cc451acafd682..86d62327d7fbd 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -2,20 +2,11 @@ // 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 '../browser_detection.dart'; -import '../configuration.dart'; import '../dom.dart'; -import '../platform_dispatcher.dart'; -import '../util.dart'; import '../window.dart'; -import 'canvas.dart'; -import 'canvaskit_api.dart'; import 'renderer.dart'; -import 'surface_factory.dart'; import 'util.dart'; /// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit @@ -54,8 +45,10 @@ class RenderCanvas { int _pixelWidth = -1; int _pixelHeight = -1; + DomCanvasBitmapRendererContext? renderContext; + ui.Size? _currentCanvasPhysicalSize; - ui.Size? _currentSurfaceSize; + ui.Size? _currentRenderSize; double _currentDevicePixelRatio = -1; bool _addedToScene = false; @@ -83,51 +76,79 @@ class RenderCanvas { style.height = '${logicalHeight}px'; } - /// Creates a and SkSurface for the given [size]. - CkSurface createOrUpdateSurface(ui.Size size) { - if (size.isEmpty) { - throw CanvasKitError('Cannot create surfaces of empty size.'); + /// This function is expensive. + /// + /// 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 (canvasElement != null) { + canvasElement!.remove(); } - // Check if the window is the same size as before, and if so, don't allocate - // a new canvas as the previous canvas is big enough to fit everything. - final ui.Size? previousSurfaceSize = _currentSurfaceSize; - 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!; - } + // If `physicalSize` is not precise, use a slightly bigger canvas. This way + // we ensure that the rendred picture covers the entire browser window. + _pixelWidth = physicalSize.width.ceil(); + _pixelHeight = physicalSize.height.ceil(); + final DomCanvasElement htmlCanvas = createDomCanvasElement( + width: _pixelWidth, + height: _pixelHeight, + ); + canvasElement = htmlCanvas; + renderContext = htmlCanvas.bitmapRendererContext; + + // 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(); + htmlElement.append(htmlCanvas); + } - final ui.Size? previousCanvasSize = _currentCanvasPhysicalSize; - // Initialize a new, larger, canvas. If the size is growing, then make the - // new canvas larger than required to avoid many canvas creations. - if (previousCanvasSize != null && - (size.width > previousCanvasSize.width || - size.height > previousCanvasSize.height)) { - final ui.Size newSize = size * 1.4; - _surface?.dispose(); - _surface = null; - offscreenCanvas!.width = newSize.width; - offscreenCanvas!.height = newSize.height; - _currentCanvasPhysicalSize = newSize; - _pixelWidth = newSize.width.ceil(); - _pixelHeight = newSize.height.ceil(); + /// Ensures that this canvas can draw a frame of the given [size]. + void ensureSize(ui.Size size) { + if (size.isEmpty) { + throw CanvasKitError('Cannot create canvases of empty size.'); + } + + // Check if the window is the same size as before, and if so, don't allocate + // a new canvas as the previous canvas is big enough to fit everything. + final ui.Size? previousRenderSize = _currentRenderSize; + if (previousRenderSize != null && + size.width == previousRenderSize.width && + size.height == previousRenderSize.height) { + // The existing surface is still reusable. + if (window.devicePixelRatio != _currentDevicePixelRatio) { _updateLogicalHtmlCanvasSize(); } + return; + } + + final ui.Size? previousCanvasSize = _currentCanvasPhysicalSize; + // Initialize a new, larger, canvas. If the size is growing, then make the + // new canvas larger than required to avoid many canvas creations. + if (previousCanvasSize != null && + (size.width > previousCanvasSize.width || + size.height > previousCanvasSize.height)) { + final ui.Size newSize = size * 1.4; + canvasElement!.width = newSize.width; + canvasElement!.height = newSize.height; + _currentCanvasPhysicalSize = newSize; + _pixelWidth = newSize.width.ceil(); + _pixelHeight = newSize.height.ceil(); + _updateLogicalHtmlCanvasSize(); + } // This is the first frame we have rendered with this canvas. if (_currentCanvasPhysicalSize == null) { - _surface?.dispose(); - _surface = null; _addedToScene = false; - _grContext?.releaseResourcesAndAbandonContext(); - _grContext?.delete(); - _grContext = null; _createNewCanvas(size); _currentCanvasPhysicalSize = size; @@ -136,11 +157,7 @@ class RenderCanvas { } _currentDevicePixelRatio = window.devicePixelRatio; - _currentSurfaceSize = size; - _translateCanvas(); - _surface?.dispose(); - _surface = _createNewSurface(size); - return _surface!; + _currentRenderSize = size; } void dispose() { diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 12ff705e45190..18518c9d35dba 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -177,11 +177,6 @@ class Surface { if (previousSurfaceSize != null && size.width == previousSurfaceSize.width && size.height == previousSurfaceSize.height) { - // The existing surface is still reusable. - if (window.devicePixelRatio != _currentDevicePixelRatio) { - _updateLogicalHtmlCanvasSize(); - _translateCanvas(); - } return _surface!; } @@ -199,7 +194,6 @@ class Surface { _currentCanvasPhysicalSize = newSize; _pixelWidth = newSize.width.ceil(); _pixelHeight = newSize.height.ceil(); - _updateLogicalHtmlCanvasSize(); } } @@ -215,49 +209,15 @@ class Surface { _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 = offscreenCanvas!.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; - offscreenCanvas!.style.transform = 'translate(0, -${offset}px)'; - } - JSVoid _contextRestoredListener(DomEvent event) { assert( _contextLost, @@ -273,14 +233,9 @@ class Surface { JSVoid _contextLostListener(DomEvent event) { assert(event.target == offscreenCanvas, 'Received a context lost event for a disposed canvas'); - final RenderCanvasFactory factory = RenderCanvasFactory.instance; _contextLost = true; - if (factory.isLive(this)) { - _forceNewContext = true; - event.preventDefault(); - } else { - dispose(); - } + _forceNewContext = true; + event.preventDefault(); } /// This function is expensive. @@ -288,18 +243,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 (this.offscreenCanvas != null) { - this.offscreenCanvas!.removeEventListener( + if (offscreenCanvas != null) { + offscreenCanvas!.removeEventListener( 'webglcontextrestored', _cachedContextRestoredListener, false, ); - this.offscreenCanvas!.removeEventListener( + offscreenCanvas!.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false, ); - this.offscreenCanvas!.remove(); + offscreenCanvas = null; _cachedContextRestoredListener = null; _cachedContextLostListener = null; } @@ -308,25 +263,11 @@ class Surface { // we ensure that the rendred picture covers the entire browser window. _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); - final DomCanvasElement htmlCanvas = createDomCanvasElement( - width: _pixelWidth, - height: _pixelHeight, + final DomOffscreenCanvas htmlCanvas = createDomOffscreenCanvas( + _pixelWidth, + _pixelHeight, ); - this.offscreenCanvas = 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(); + offscreenCanvas = htmlCanvas; // 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. @@ -350,7 +291,7 @@ class Surface { _contextLost = false; if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) { - final int glContext = canvasKit.GetWebGLContext( + final int glContext = canvasKit.GetOffscreenWebGLContext( htmlCanvas, SkWebGLContextOptions( // Default to no anti-aliasing. Paint commands can be explicitly @@ -376,8 +317,6 @@ class Surface { _syncCacheBytes(); } } - - htmlElement.append(htmlCanvas); } void _initWebglParams() { @@ -419,13 +358,13 @@ class Surface { static bool _didWarnAboutWebGlInitializationFailure = false; CkSurface _makeSoftwareCanvasSurface( - DomCanvasElement htmlCanvas, String reason) { + DomOffscreenCanvas htmlCanvas, String reason) { if (!_didWarnAboutWebGlInitializationFailure) { printWarning('WARNING: Falling back to CPU-only rendering. $reason.'); _didWarnAboutWebGlInitializationFailure = true; } return CkSurface( - canvasKit.MakeSWCanvasSurface(htmlCanvas), + canvasKit.MakeOffscreenSWCanvasSurface(htmlCanvas), null, ); } diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 26ed2f9674025..cec49593b5d84 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -1073,6 +1073,23 @@ extension DomCanvasElementExtension on DomCanvasElement { } return getContext('webgl2')! as WebGLContext; } + + DomCanvasBitmapRendererContext get bitmapRendererContext => + getContext('bitmaprenderer')! as DomCanvasBitmapRendererContext; +} + +@JS() +@staticInterop +class DomImageBitmap {} + +@JS() +@staticInterop +class DomCanvasBitmapRendererContext {} + +extension DomCanvasBitmapRendererContextExtension on DomCanvasBitmapRendererContext { + @JS('transferFromImageBitmap') + external void _transferFromImageBitmap(JSAny? bitmap); + void transferFromImageBitmap(DomImageBitmap bitmap) => _transferFromImageBitmap(bitmap.toJSAnyShallow); } @JS() @@ -2722,6 +2739,13 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas { } } + WebGLContext getGlContext(int majorVersion) { + if (majorVersion == 1) { + return getContext('webgl')! as WebGLContext; + } + return getContext('webgl2')! as WebGLContext; + } + @JS('convertToBlob') external JSPromise _convertToBlob1(); @JS('convertToBlob') @@ -2735,6 +2759,10 @@ 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) => diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index c6fcfb3d85dfa..a9cc26764dccb 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -477,7 +477,6 @@ void testMain() { // Render: Views 1-10 // Expect: main canvas plus platform view overlays; empty cache. renderTestScene([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - expect(RenderCanvasFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -504,7 +503,6 @@ void testMain() { // Expect: main canvas plus platform view overlays; empty cache. await Future.delayed(Duration.zero); renderTestScene([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(RenderCanvasFactory.instance.numAvailableOverlays, 0); _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, @@ -782,28 +780,6 @@ void testMain() { ]); }); - test('does not crash when overlays are disabled', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - HtmlViewEmbedder.debugDisableOverlays = true; - ui_web.platformViewRegistry.registerViewFactory( - 'test-platform-view', - (int viewId) => createDomHTMLDivElement()..id = 'view-0', - ); - await createPlatformView(0, 'test-platform-view'); - - final LayerSceneBuilder sb = LayerSceneBuilder(); - sb.pushOffset(0, 0); - sb.addPlatformView(0, width: 10, height: 10); - sb.pop(); - // The below line should not throw an error. - rasterizer.draw(sb.build().layerTree); - _expectSceneMatches(<_EmbeddedViewMarker>[ - _overlay, - _platformView, - ]); - HtmlViewEmbedder.debugDisableOverlays = false; - }); - test('works correctly with max overlays == 2', () async { final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; final FlutterConfiguration config = FlutterConfiguration() @@ -815,9 +791,6 @@ void testMain() { RenderCanvasFactory.instance.debugClear(); - expect(RenderCanvasFactory.instance.maximumSurfaces, 2); - expect(RenderCanvasFactory.instance.maximumOverlays, 1); - ui_web.platformViewRegistry.registerViewFactory( 'test-platform-view', (int viewId) => createDomHTMLDivElement()..id = 'view-0', @@ -857,45 +830,6 @@ void testMain() { debugSetConfiguration(FlutterConfiguration()); }); - 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( diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart index 1338b2559ce1e..bd15218141df3 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -72,15 +72,15 @@ void testMain() { expect(RenderCanvasFactory.debugUninitializedInstance, isNotNull); // Cause the surface and its canvas to be attached to the page - originalFactory.baseCanvas.acquireFrame(const ui.Size(10, 10)); + originalFactory.baseCanvas.ensureSize(const ui.Size(10, 10)); originalFactory.baseCanvas.addToScene(); - expect(originalFactory.baseCanvas.offscreenCanvas!.isConnected, isTrue); + expect(originalFactory.baseCanvas.canvasElement!.isConnected, isTrue); // Create a few overlay canvases final List overlays = []; for (int i = 0; i < 3; i++) { - overlays.add(originalFactory.getCanvas()! - ..acquireFrame(const ui.Size(10, 10)) + overlays.add(originalFactory.getCanvas() + ..ensureSize(const ui.Size(10, 10)) ..addToScene()); } expect(originalFactory.debugSurfaceCount, 4); diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart new file mode 100644 index 0000000000000..7a4f1e9fca362 --- /dev/null +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -0,0 +1,239 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. + +// found in the LICENSE file. + +import 'dart:js_util' as js_util; + +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'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('CanvasKit', () { + setUpCanvasKitTest(); + setUp(() { + window.debugOverrideDevicePixelRatio(1.0); + }); + + test('RenderCanvas allocates canvases efficiently', () { + final RenderCanvas canvas = RenderCanvasFactory.instance.getCanvas(); + canvas.ensureSize(const ui.Size(9, 19)); + final DomCanvasElement original = canvas.canvasElement!; + + // 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')); + + // Shrinking reuses the existing canvas but translates it so + // Skia renders into the visible area. + final CkSurface shrunkSurface = + surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; + final DomCanvasElement shrunk = surface.offscreenCanvas!; + 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); + + // The first increase will allocate a new surface, but will overallocate + // by 40% to accommodate future increases. + final CkSurface firstIncreaseSurface = + surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; + final DomCanvasElement firstIncrease = surface.offscreenCanvas!; + expect(firstIncrease, same(original)); + expect(firstIncreaseSurface, isNot(same(shrunkSurface))); + + // Expect overallocated dimensions + expect(firstIncrease.width, 14); + expect(firstIncrease.height, 28); + expect(firstIncrease.style.width, '14px'); + expect(firstIncrease.style.height, '28px'); + expect(firstIncrease.style.transform, _isTranslate('0', '-8')); + expect(firstIncreaseSurface.width(), 10); + expect(firstIncreaseSurface.height(), 20); + + // Subsequent increases within 40% reuse the old canvas. + final CkSurface secondIncreaseSurface = + surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; + final DomCanvasElement secondIncrease = surface.offscreenCanvas!; + expect(secondIncrease, same(firstIncrease)); + expect(secondIncrease.style.transform, _isTranslate('0', '-6')); + expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); + expect(secondIncreaseSurface.width(), 11); + expect(secondIncreaseSurface.height(), 22); + + // Increases beyond the 40% limit will cause a new allocation. + final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; + final DomCanvasElement huge = surface.offscreenCanvas!; + expect(huge, same(secondIncrease)); + expect(hugeSurface, isNot(same(secondIncreaseSurface))); + + // Also over-allocated + expect(huge.width, 28); + expect(huge.height, 56); + expect(huge.style.width, '28px'); + expect(huge.style.height, '56px'); + expect(huge.style.transform, _isTranslate('0', '-16')); + expect(hugeSurface.width(), 20); + expect(hugeSurface.height(), 40); + + // Shrink again. Reuse the last allocated surface. + final CkSurface shrunkSurface2 = + surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; + final DomCanvasElement shrunk2 = surface.offscreenCanvas!; + 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); + + // Doubling the DPR should halve the CSS width, height, and translation of the canvas. + // This tests https://github.com/flutter/flutter/issues/77084 + window.debugOverrideDevicePixelRatio(2.0); + final CkSurface dpr2Surface2 = + surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; + final DomCanvasElement dpr2Canvas = surface.offscreenCanvas!; + 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); + + // Skipping on Firefox for now since Firefox headless doesn't support WebGL + // This causes issues in the test since we create a Canvas-backed surface, + // which cannot be a different size from the canvas. + // TODO(hterkelsen): See if we can give a custom size for software + // surfaces. + }, skip: isFirefox); + + test( + 'Surface creates new context when WebGL context is restored', + () async { + final Surface? surface = RenderCanvasFactory.instance.getCanvas(); + expect(surface!.debugForceNewContext, isTrue); + final CkSurface before = + surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; + expect(surface.debugForceNewContext, isFalse); + + // Pump a timer to flush any microtasks. + await Future.delayed(Duration.zero); + final CkSurface afterAcquireFrame = + surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; + // Existing context is reused. + expect(afterAcquireFrame, same(before)); + + // Emulate WebGL context loss. + final DomCanvasElement canvas = + surface.htmlElement.children.single as DomCanvasElement; + final Object ctx = canvas.getContext('webgl2')!; + final Object loseContextExtension = js_util.callMethod( + ctx, + 'getExtension', + ['WEBGL_lose_context'], + ); + 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 []); + expect(isContextLost, isTrue); + + // Emulate WebGL context restoration. + js_util.callMethod(loseContextExtension, 'restoreContext', const []); + + // Pump a timer to allow the "restore context" event to propagate. + await Future.delayed(Duration.zero); + expect(surface.debugForceNewContext, isTrue); + + final CkSurface afterContextLost = + surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; + // A new context is created. + expect(afterContextLost, isNot(same(before))); + }, + // Firefox can't create a WebGL2 context in headless mode. + skip: isFirefox, + ); + + // 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; + + expect(original.width(), 10); + expect(original.height(), 16); + expect(surface.offscreenCanvas!.style.width, '10px'); + expect(surface.offscreenCanvas!.style.height, '16px'); + expect(surface.offscreenCanvas!.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. + window.debugOverrideDevicePixelRatio(2.0); + final CkSurface highDpr = + surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; + expect(highDpr.width(), 10); + expect(highDpr.height(), 16); + expect(surface.offscreenCanvas!.style.width, '5px'); + expect(surface.offscreenCanvas!.style.height, '8px'); + expect(surface.offscreenCanvas!.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. + window.debugOverrideDevicePixelRatio(0.5); + final CkSurface lowDpr = + surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; + expect(lowDpr.width(), 10); + expect(lowDpr.height(), 16); + expect(surface.offscreenCanvas!.style.width, '20px'); + expect(surface.offscreenCanvas!.style.height, '32px'); + expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + + // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 + window.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.offscreenCanvas!.style.width, '5px'); + expect(surface.offscreenCanvas!.style.height, '8px'); + expect(surface.offscreenCanvas!.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/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index 22f49fee75001..845ab52fec36f 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -1,5 +1,5 @@ // 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_util' as js_util; @@ -23,7 +23,7 @@ void testMain() { }); test('Surface allocates canvases efficiently', () { - final Surface? surface = RenderCanvasFactory.instance.getCanvas(); + final Surface? surface = Surface(); final CkSurface originalSurface = surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; final DomCanvasElement original = surface.offscreenCanvas!; From a9ada4771c7ed16723800ddf10ad31ef77f82ad5 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 8 Jun 2023 13:00:13 -0700 Subject: [PATCH 05/30] update tests --- .../lib/src/engine/canvaskit/surface.dart | 24 -- lib/web_ui/lib/src/engine/configuration.dart | 13 +- .../test/canvaskit/embedded_views_test.dart | 74 ++---- .../test/canvaskit/render_canvas_test.dart | 221 ++---------------- lib/web_ui/test/canvaskit/surface_test.dart | 83 ++----- 5 files changed, 67 insertions(+), 348 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 18518c9d35dba..e193ce8b6385e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -14,8 +14,6 @@ import '../util.dart'; import '../window.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; -import 'render_canvas_factory.dart'; -import 'renderer.dart'; import 'util.dart'; // Only supported in profile/release mode. Allows Flutter to use MSAA but @@ -83,17 +81,6 @@ class Surface { int? _glContext; int? _skiaCacheBytes; - /// The root HTML element for this surface. - /// - /// This element contains the canvas used to draw the UI. Unlike the canvas, - /// this element is permanent. It is never replaced or deleted, until this - /// surface is disposed of via [dispose]. - /// - /// Conversely, the canvas that lives inside this element can be swapped, for - /// example, when the screen size changes, or when the WebGL context is lost - /// due to the browser tab becoming dormant. - final DomElement htmlElement = createDomElement('flt-canvas-container'); - /// The underlying `` element used for this surface. DomOffscreenCanvas? offscreenCanvas; int _pixelWidth = -1; @@ -113,8 +100,6 @@ class Surface { } } - bool _addedToScene = false; - /// Acquire a frame of the given [size] containing a drawable canvas. /// /// The given [size] is in physical pixels. @@ -130,13 +115,6 @@ 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; @@ -201,7 +179,6 @@ class Surface { if (_forceNewContext || _currentCanvasPhysicalSize == null) { _surface?.dispose(); _surface = null; - _addedToScene = false; _grContext?.releaseResourcesAndAbandonContext(); _grContext?.delete(); _grContext = null; @@ -381,7 +358,6 @@ class Surface { 'webglcontextrestored', _cachedContextRestoredListener, false); _cachedContextLostListener = null; _cachedContextRestoredListener = null; - htmlElement.remove(); _surface?.dispose(); } } diff --git a/lib/web_ui/lib/src/engine/configuration.dart b/lib/web_ui/lib/src/engine/configuration.dart index 62b05c7a4fc0f..66d387215fd97 100644 --- a/lib/web_ui/lib/src/engine/configuration.dart +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -233,15 +233,10 @@ class FlutterConfiguration { 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', ); - /// The maximum number of overlay surfaces that the CanvasKit renderer will use. - /// - /// Overlay surfaces are extra WebGL `` elements used to paint on top - /// of platform views. Too many platform views can cause the browser to run - /// out of resources (memory, CPU, GPU) to handle the content efficiently. - /// The number of overlay surfaces is therefore limited. - /// - /// This value can be specified using either the `FLUTTER_WEB_MAXIMUM_SURFACES` - /// environment variable, or using the runtime configuration. + /// This is deprecated. The CanvasKit renderer will only ever create one + /// WebGL context, obviating the problem this configuration was meant to + /// solve originally. + @Deprecated('Setting canvasKitMaximumSurfaces has no effect') int get canvasKitMaximumSurfaces => _configuration?.canvasKitMaximumSurfaces?.toInt() ?? _defaultCanvasKitMaximumSurfaces; static const int _defaultCanvasKitMaximumSurfaces = int.fromEnvironment( diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index a9cc26764dccb..f57e6074e85a2 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:js/js_util.dart' as js_util; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -293,7 +292,6 @@ void testMain() { test('renders overlays on top of platform views', () async { expect(RenderCanvasFactory.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()); @@ -341,6 +339,7 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, ]); // Frame 2: @@ -393,14 +392,23 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, ]); // Frame 5: @@ -494,8 +502,11 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, ]); // Frame 2: @@ -520,8 +531,11 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, ]); // Frame 3: @@ -546,8 +560,11 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, ]); // Frame 4: @@ -572,8 +589,11 @@ void testMain() { _platformView, _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, + _overlay, ]); // TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867 @@ -780,56 +800,6 @@ void testMain() { ]); }); - test('works correctly with max overlays == 2', () async { - final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; - final FlutterConfiguration config = FlutterConfiguration() - ..setUserConfiguration( - js_util.jsify({ - 'canvasKitMaximumSurfaces': 2, - }) as JsFlutterConfiguration); - debugSetConfiguration(config); - - RenderCanvasFactory.instance.debugClear(); - - 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 - debugSetConfiguration(FlutterConfiguration()); - }); - test('does not create overlays for invisible platform views', () async { final Rasterizer rasterizer = CanvasKitRenderer.instance.rasterizer; ui_web.platformViewRegistry.registerViewFactory( diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index 7a4f1e9fca362..cfa750c25e66e 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -2,8 +2,6 @@ // found in the LICENSE file. -import 'dart:js_util' as js_util; - import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -22,218 +20,41 @@ void testMain() { window.debugOverrideDevicePixelRatio(1.0); }); - test('RenderCanvas allocates canvases efficiently', () { - final RenderCanvas canvas = RenderCanvasFactory.instance.getCanvas(); - canvas.ensureSize(const ui.Size(9, 19)); - final DomCanvasElement original = canvas.canvasElement!; - - // 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')); - - // Shrinking reuses the existing canvas but translates it so - // Skia renders into the visible area. - final CkSurface shrunkSurface = - surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk = surface.offscreenCanvas!; - 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); - - // The first increase will allocate a new surface, but will overallocate - // by 40% to accommodate future increases. - final CkSurface firstIncreaseSurface = - surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomCanvasElement firstIncrease = surface.offscreenCanvas!; - expect(firstIncrease, same(original)); - expect(firstIncreaseSurface, isNot(same(shrunkSurface))); - - // Expect overallocated dimensions - expect(firstIncrease.width, 14); - expect(firstIncrease.height, 28); - expect(firstIncrease.style.width, '14px'); - expect(firstIncrease.style.height, '28px'); - expect(firstIncrease.style.transform, _isTranslate('0', '-8')); - expect(firstIncreaseSurface.width(), 10); - expect(firstIncreaseSurface.height(), 20); - - // Subsequent increases within 40% reuse the old canvas. - final CkSurface secondIncreaseSurface = - surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; - final DomCanvasElement secondIncrease = surface.offscreenCanvas!; - expect(secondIncrease, same(firstIncrease)); - expect(secondIncrease.style.transform, _isTranslate('0', '-6')); - expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); - expect(secondIncreaseSurface.width(), 11); - expect(secondIncreaseSurface.height(), 22); - - // Increases beyond the 40% limit will cause a new allocation. - final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; - final DomCanvasElement huge = surface.offscreenCanvas!; - expect(huge, same(secondIncrease)); - expect(hugeSurface, isNot(same(secondIncreaseSurface))); - - // Also over-allocated - expect(huge.width, 28); - expect(huge.height, 56); - expect(huge.style.width, '28px'); - expect(huge.style.height, '56px'); - expect(huge.style.transform, _isTranslate('0', '-16')); - expect(hugeSurface.width(), 20); - expect(hugeSurface.height(), 40); - - // Shrink again. Reuse the last allocated surface. - final CkSurface shrunkSurface2 = - surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk2 = surface.offscreenCanvas!; - 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); - - // Doubling the DPR should halve the CSS width, height, and translation of the canvas. - // This tests https://github.com/flutter/flutter/issues/77084 - window.debugOverrideDevicePixelRatio(2.0); - final CkSurface dpr2Surface2 = - surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement dpr2Canvas = surface.offscreenCanvas!; - 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); - - // Skipping on Firefox for now since Firefox headless doesn't support WebGL - // This causes issues in the test since we create a Canvas-backed surface, - // which cannot be a different size from the canvas. - // TODO(hterkelsen): See if we can give a custom size for software - // surfaces. - }, skip: isFirefox); - - test( - 'Surface creates new context when WebGL context is restored', - () async { - final Surface? surface = RenderCanvasFactory.instance.getCanvas(); - expect(surface!.debugForceNewContext, isTrue); - final CkSurface before = - surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; - expect(surface.debugForceNewContext, isFalse); - - // Pump a timer to flush any microtasks. - await Future.delayed(Duration.zero); - final CkSurface afterAcquireFrame = - surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; - // Existing context is reused. - expect(afterAcquireFrame, same(before)); - - // Emulate WebGL context loss. - final DomCanvasElement canvas = - surface.htmlElement.children.single as DomCanvasElement; - final Object ctx = canvas.getContext('webgl2')!; - final Object loseContextExtension = js_util.callMethod( - ctx, - 'getExtension', - ['WEBGL_lose_context'], - ); - 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 []); - expect(isContextLost, isTrue); - - // Emulate WebGL context restoration. - js_util.callMethod(loseContextExtension, 'restoreContext', const []); - - // Pump a timer to allow the "restore context" event to propagate. - await Future.delayed(Duration.zero); - expect(surface.debugForceNewContext, isTrue); - - final CkSurface afterContextLost = - surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; - // A new context is created. - expect(afterContextLost, isNot(same(before))); - }, - // Firefox can't create a WebGL2 context in headless mode. - skip: isFirefox, - ); - // 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; + final RenderCanvas canvas = RenderCanvas(); + canvas.ensureSize(const ui.Size(10, 16)); - expect(original.width(), 10); - expect(original.height(), 16); - expect(surface.offscreenCanvas!.style.width, '10px'); - expect(surface.offscreenCanvas!.style.height, '16px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + 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); - final CkSurface highDpr = - surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; - expect(highDpr.width(), 10); - expect(highDpr.height(), 16); - expect(surface.offscreenCanvas!.style.width, '5px'); - expect(surface.offscreenCanvas!.style.height, '8px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + canvas.ensureSize(const ui.Size(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); - final CkSurface lowDpr = - surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; - expect(lowDpr.width(), 10); - expect(lowDpr.height(), 16); - expect(surface.offscreenCanvas!.style.width, '20px'); - expect(surface.offscreenCanvas!.style.height, '32px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + canvas.ensureSize(const ui.Size(10, 16)); + expect(canvas.canvasElement!.width, 10); + expect(canvas.canvasElement!.height, 16); + expect(canvas.canvasElement!.style.width, '20px'); + expect(canvas.canvasElement!.style.height, '32px'); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.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.offscreenCanvas!.style.width, '5px'); - expect(surface.offscreenCanvas!.style.height, '8px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + canvas.ensureSize(const ui.Size(9.9, 15.9)); + expect(canvas.canvasElement!.width, 10); + expect(canvas.canvasElement!.height, 16); + expect(canvas.canvasElement!.style.width, '5px'); + expect(canvas.canvasElement!.style.height, '8px'); }); }); } - -/// 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/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index 845ab52fec36f..95955df9cab4f 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -23,17 +23,14 @@ void testMain() { }); test('Surface allocates canvases efficiently', () { - final Surface? surface = Surface(); + final Surface surface = Surface(); final CkSurface originalSurface = - surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface; - final DomCanvasElement original = surface.offscreenCanvas!; + surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; + final DomOffscreenCanvas original = surface.offscreenCanvas!; // Expect exact requested dimensions. expect(original.width, 9); expect(original.height, 19); - expect(original.style.width, '9px'); - expect(original.style.height, '19px'); - expect(original.style.transform, _isTranslate('0', '0')); expect(originalSurface.width(), 9); expect(originalSurface.height(), 19); @@ -41,11 +38,8 @@ void testMain() { // Skia renders into the visible area. final CkSurface shrunkSurface = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk = surface.offscreenCanvas!; + final DomOffscreenCanvas shrunk = surface.offscreenCanvas!; expect(shrunk, same(original)); - expect(shrunk.style.width, '9px'); - expect(shrunk.style.height, '19px'); - expect(shrunk.style.transform, _isTranslate('0', '-4')); expect(shrunkSurface, isNot(same(originalSurface))); expect(shrunkSurface.width(), 5); expect(shrunkSurface.height(), 15); @@ -54,52 +48,42 @@ void testMain() { // by 40% to accommodate future increases. final CkSurface firstIncreaseSurface = surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomCanvasElement firstIncrease = surface.offscreenCanvas!; + final DomOffscreenCanvas firstIncrease = surface.offscreenCanvas!; expect(firstIncrease, same(original)); expect(firstIncreaseSurface, isNot(same(shrunkSurface))); // Expect overallocated dimensions expect(firstIncrease.width, 14); expect(firstIncrease.height, 28); - expect(firstIncrease.style.width, '14px'); - expect(firstIncrease.style.height, '28px'); - expect(firstIncrease.style.transform, _isTranslate('0', '-8')); expect(firstIncreaseSurface.width(), 10); expect(firstIncreaseSurface.height(), 20); // Subsequent increases within 40% reuse the old canvas. final CkSurface secondIncreaseSurface = surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; - final DomCanvasElement secondIncrease = surface.offscreenCanvas!; + final DomOffscreenCanvas secondIncrease = surface.offscreenCanvas!; expect(secondIncrease, same(firstIncrease)); - expect(secondIncrease.style.transform, _isTranslate('0', '-6')); expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); expect(secondIncreaseSurface.width(), 11); expect(secondIncreaseSurface.height(), 22); // Increases beyond the 40% limit will cause a new allocation. final CkSurface hugeSurface = surface.acquireFrame(const ui.Size(20, 40)).skiaSurface; - final DomCanvasElement huge = surface.offscreenCanvas!; + final DomOffscreenCanvas huge = surface.offscreenCanvas!; expect(huge, same(secondIncrease)); expect(hugeSurface, isNot(same(secondIncreaseSurface))); // Also over-allocated expect(huge.width, 28); expect(huge.height, 56); - expect(huge.style.width, '28px'); - expect(huge.style.height, '56px'); - expect(huge.style.transform, _isTranslate('0', '-16')); expect(hugeSurface.width(), 20); expect(hugeSurface.height(), 40); // Shrink again. Reuse the last allocated surface. final CkSurface shrunkSurface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement shrunk2 = surface.offscreenCanvas!; + final DomOffscreenCanvas shrunk2 = surface.offscreenCanvas!; expect(shrunk2, same(huge)); - expect(shrunk2.style.width, '28px'); - expect(shrunk2.style.height, '56px'); - expect(shrunk2.style.transform, _isTranslate('0', '-41')); expect(shrunkSurface2, isNot(same(hugeSurface))); expect(shrunkSurface2.width(), 5); expect(shrunkSurface2.height(), 15); @@ -109,11 +93,8 @@ void testMain() { window.debugOverrideDevicePixelRatio(2.0); final CkSurface dpr2Surface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomCanvasElement dpr2Canvas = surface.offscreenCanvas!; + final DomOffscreenCanvas dpr2Canvas = surface.offscreenCanvas!; 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); @@ -128,8 +109,8 @@ void testMain() { test( 'Surface creates new context when WebGL context is restored', () async { - final Surface? surface = RenderCanvasFactory.instance.getCanvas(); - expect(surface!.debugForceNewContext, isTrue); + final Surface surface = Surface(); + expect(surface.debugForceNewContext, isTrue); final CkSurface before = surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; expect(surface.debugForceNewContext, isFalse); @@ -142,8 +123,7 @@ void testMain() { expect(afterAcquireFrame, same(before)); // Emulate WebGL context loss. - final DomCanvasElement canvas = - surface.htmlElement.children.single as DomCanvasElement; + final DomOffscreenCanvas canvas = surface.offscreenCanvas!; final Object ctx = canvas.getContext('webgl2')!; final Object loseContextExtension = js_util.callMethod( ctx, @@ -183,9 +163,8 @@ void testMain() { expect(original.width(), 10); expect(original.height(), 16); - expect(surface.offscreenCanvas!.style.width, '10px'); - expect(surface.offscreenCanvas!.style.height, '16px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.width, 10); + expect(surface.offscreenCanvas!.height, 16); // Increase device-pixel ratio: this makes CSS pixels bigger, so we need // fewer of them to cover the browser window. @@ -194,9 +173,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(highDpr.width(), 10); expect(highDpr.height(), 16); - expect(surface.offscreenCanvas!.style.width, '5px'); - expect(surface.offscreenCanvas!.style.height, '8px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.width, 10); + expect(surface.offscreenCanvas!.height, 16); // Decrease device-pixel ratio: this makes CSS pixels smaller, so we need // more of them to cover the browser window. @@ -205,9 +183,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(lowDpr.width(), 10); expect(lowDpr.height(), 16); - expect(surface.offscreenCanvas!.style.width, '20px'); - expect(surface.offscreenCanvas!.style.height, '32px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.width, 10); + expect(surface.offscreenCanvas!.height, 16); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); @@ -215,28 +192,8 @@ void testMain() { surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; expect(changeRatioAndSize.width(), 10); expect(changeRatioAndSize.height(), 16); - expect(surface.offscreenCanvas!.style.width, '5px'); - expect(surface.offscreenCanvas!.style.height, '8px'); - expect(surface.offscreenCanvas!.style.transform, _isTranslate('0', '0')); + expect(surface.offscreenCanvas!.width, 10); + expect(surface.offscreenCanvas!.height, 16); }); }); } - -/// 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. - ); - } -} From 4e332d62891abf5198a6bbed6839ddea49c357cc Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Mon, 12 Jun 2023 14:30:36 -0700 Subject: [PATCH 06/30] Update goldens --- ci/licenses_golden/licenses_flutter | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 5000803f207e8..b18d50a63c3f6 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1919,10 +1919,11 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart + ../../../flutter/LICENSE @@ -4596,10 +4597,11 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/platform_message.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text_fragmenter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/util.dart From 911871e27922c2f0fa4a9cdfd825efe4bd9a3698 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Mon, 12 Jun 2023 16:27:23 -0700 Subject: [PATCH 07/30] Fix license header --- lib/web_ui/test/canvaskit/render_canvas_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index cfa750c25e66e..35d3a32056a7c 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -1,5 +1,5 @@ // 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'; From 32f6d1b42d9f4760d30d27ae2c2edad7a4a22b7a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 13 Jun 2023 10:55:58 -0700 Subject: [PATCH 08/30] Fix license header --- lib/web_ui/test/canvaskit/surface_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index 95955df9cab4f..4ef9c5c49b754 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -1,5 +1,5 @@ // 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_util' as js_util; From 84940bcf68e14db8de28ae63ce506c83563720d9 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 13 Jun 2023 13:31:49 -0700 Subject: [PATCH 09/30] Address analyzer warnings --- .../src/engine/canvaskit/embedded_views.dart | 20 ++------ .../lib/src/engine/canvaskit/rasterizer.dart | 51 +++++++++++-------- .../canvaskit/render_canvas_factory.dart | 1 - 3 files changed, 34 insertions(+), 38 deletions(-) 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 2e67df7535a17..035aa5e74eee8 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -367,26 +367,14 @@ class HtmlViewEmbedder { 'There should be the same number of picture recorders ' '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).', ); - int pictureRecorderIndex = 0; - - // Prime the SkSurface for rendering. - final SurfaceFrame frame = CanvasKitRenderer - .instance.rasterizer.offscreenSurface - .acquireFrame(_frameSize); + int pictureRecorderIndex = 0; for (int i = 0; i < _compositionOrder.length; i++) { final int viewId = _compositionOrder[i]; if (_overlays[viewId] != null) { - // Render the picture to the canvas - _overlays[viewId]!.ensureSize(_frameSize); - final CkCanvas canvas = frame.skiaCanvas; - canvas.clear(const ui.Color(0x00000000)); - canvas.drawPicture( - _context.pictureRecorders[pictureRecorderIndex].endRecording(), - ); - frame.submit(); - final DomImageBitmap bitmap = CanvasKitRenderer.instance.rasterizer.offscreenSurface.offscreenCanvas!.transferToImageBitmap(); - _overlays[viewId]!.renderContext!.transferFromImageBitmap(bitmap); + CanvasKitRenderer.instance.rasterizer.rasterizeToCanvas( + _overlays[viewId]!, + _context.pictureRecorders[pictureRecorderIndex].endRecording()); pictureRecorderIndex++; } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 01c1c403c9671..e92594cd72ced 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -6,13 +6,6 @@ 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 'render_canvas_factory.dart'; -import 'surface.dart'; - /// A class that can rasterize [LayerTree]s into a given [Surface]. class Rasterizer { final CompositorContext context = CompositorContext(); @@ -20,10 +13,28 @@ class 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(); + final Surface _offscreenSurface = Surface(); + ui.Size _currentFrameSize = ui.Size.zero; + late SurfaceFrame _currentFrame; + + /// Render the given [picture] so it is displayed by the given [canvas]. + void rasterizeToCanvas(RenderCanvas canvas, CkPicture picture) { + // Ensure the [canvas] is the correct size. + canvas.ensureSize(_currentFrameSize); + + final CkCanvas skCanvas = _currentFrame.skiaCanvas; + skCanvas.clear(const ui.Color(0x00000000)); + skCanvas.drawPicture(picture); + _currentFrame.submit(); + final DomImageBitmap bitmap = + _offscreenSurface.offscreenCanvas!.transferToImageBitmap(); + canvas.renderContext!.transferFromImageBitmap(bitmap); + } + + /// Sets the maximum size of the Skia resource cache, in bytes. void setSkiaResourceCacheMaxBytes(int bytes) => - offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); + _offscreenSurface.setSkiaResourceCacheMaxBytes(bytes); /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. @@ -34,24 +45,22 @@ class Rasterizer { return; } - SurfaceFrame frame = offscreenSurface.acquireFrame(layerTree.frameSize); - RenderCanvasFactory.instance.baseCanvas.ensureSize(layerTree.frameSize); - HtmlViewEmbedder.instance.frameSize = layerTree.frameSize; + _currentFrameSize = layerTree.frameSize; + _currentFrame = _offscreenSurface.acquireFrame(_currentFrameSize); + RenderCanvasFactory.instance.baseCanvas.ensureSize(_currentFrameSize); + HtmlViewEmbedder.instance.frameSize = _currentFrameSize; final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & layerTree.frameSize); + pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000)); - final Frame compositorFrame = - context.acquireFrame(pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance); + final Frame compositorFrame = context.acquireFrame( + pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance); compositorFrame.raster(layerTree, ignoreRasterCache: true); + RenderCanvasFactory.instance.baseCanvas.addToScene(); + rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, + pictureRecorder.endRecording()); - final CkCanvas canvas = frame.skiaCanvas; - canvas.clear(const ui.Color(0x00000000)); - canvas.drawPicture(pictureRecorder.endRecording()); - frame.submit(); - final DomImageBitmap bitmap = CanvasKitRenderer.instance.rasterizer.offscreenSurface.offscreenCanvas!.transferToImageBitmap(); - RenderCanvasFactory.instance.baseCanvas.renderContext!.transferFromImageBitmap(bitmap); HtmlViewEmbedder.instance.submitFrame(); } finally { _runPostFrameCallbacks(); 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 index 5116abfe5d06c..593390972377c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas_factory.dart @@ -4,7 +4,6 @@ import 'package:meta/meta.dart'; import '../../engine.dart'; -import 'render_canvas.dart'; /// Caches canvases used to overlay platform views. class RenderCanvasFactory { From 849005f63c014e8b50b055d43e4ab6867e3a3fdb Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 13 Jun 2023 13:33:30 -0700 Subject: [PATCH 10/30] Unused import --- lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index 035aa5e74eee8..80c345fc24638 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -19,7 +19,6 @@ import 'picture_recorder.dart'; import 'render_canvas.dart'; import 'render_canvas_factory.dart'; import 'renderer.dart'; -import 'surface.dart'; /// This composites HTML views into the [ui.Scene]. class HtmlViewEmbedder { From 89895f3df43a8695441102ba5b2d4e86caacd3fd Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 13 Jun 2023 15:17:41 -0700 Subject: [PATCH 11/30] error if OffscreenCanvas is unsupported --- .../lib/src/engine/canvaskit/surface.dart | 49 ++++++++++--------- lib/web_ui/lib/src/engine/dom.dart | 47 +++++++++++------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index e193ce8b6385e..00ab0a00f25ae 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -24,8 +24,7 @@ typedef SubmitCallback = bool Function(SurfaceFrame, CkCanvas); /// A frame which contains a canvas to be drawn into. class SurfaceFrame { - SurfaceFrame(this.skiaSurface, this.submitCallback) - : _submitted = false; + SurfaceFrame(this.skiaSurface, this.submitCallback) : _submitted = false; final CkSurface skiaSurface; final SubmitCallback submitCallback; @@ -33,7 +32,6 @@ class SurfaceFrame { /// Submit this frame to be drawn. bool submit() { - if (_submitted) { return false; } @@ -121,8 +119,11 @@ class Surface { /// 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]. /// @@ -185,8 +186,7 @@ class Surface { _createNewCanvas(size); _currentCanvasPhysicalSize = size; - } else if (window.devicePixelRatio != _currentDevicePixelRatio) { - } + } else if (window.devicePixelRatio != _currentDevicePixelRatio) {} _currentDevicePixelRatio = window.devicePixelRatio; _currentSurfaceSize = size; @@ -222,15 +222,15 @@ class Surface { // Clear the container, if it's not empty. We're going to create a new . if (offscreenCanvas != null) { offscreenCanvas!.removeEventListener( - 'webglcontextrestored', - _cachedContextRestoredListener, - false, - ); + 'webglcontextrestored', + _cachedContextRestoredListener, + false, + ); offscreenCanvas!.removeEventListener( - 'webglcontextlost', - _cachedContextLostListener, - false, - ); + 'webglcontextlost', + _cachedContextLostListener, + false, + ); offscreenCanvas = null; _cachedContextRestoredListener = null; _cachedContextLostListener = null; @@ -240,6 +240,9 @@ class Surface { // we ensure that the rendred picture covers the entire browser window. _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); + if (!browserSupportsOffscreenCanvas) { + throw Exception('OffscreenCanvas is not supported'); + } final DomOffscreenCanvas htmlCanvas = createDomOffscreenCanvas( _pixelWidth, _pixelHeight, @@ -252,7 +255,8 @@ class Surface { // notification. When we receive this notification we force a new context. // // See also: https://www.khronos.org/webgl/wiki/HandlingContextLost - _cachedContextRestoredListener = createDomEventListener(_contextRestoredListener); + _cachedContextRestoredListener = + createDomEventListener(_contextRestoredListener); _cachedContextLostListener = createDomEventListener(_contextLostListener); htmlCanvas.addEventListener( 'webglcontextlost', @@ -315,13 +319,12 @@ class Surface { offscreenCanvas!, '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( diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index cec49593b5d84..6123b2fa73143 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -1086,10 +1086,12 @@ class DomImageBitmap {} @staticInterop class DomCanvasBitmapRendererContext {} -extension DomCanvasBitmapRendererContextExtension on DomCanvasBitmapRendererContext { +extension DomCanvasBitmapRendererContextExtension + on DomCanvasBitmapRendererContext { @JS('transferFromImageBitmap') external void _transferFromImageBitmap(JSAny? bitmap); - void transferFromImageBitmap(DomImageBitmap bitmap) => _transferFromImageBitmap(bitmap.toJSAnyShallow); + void transferFromImageBitmap(DomImageBitmap bitmap) => + _transferFromImageBitmap(bitmap.toJSAnyShallow); } @JS() @@ -1436,7 +1438,8 @@ MockHttpFetchResponseFactory? mockHttpFetchResponseFactory; /// [httpFetchText] instead. Future httpFetch(String url) async { if (mockHttpFetchResponseFactory != null) { - final MockHttpFetchResponse? response = await mockHttpFetchResponseFactory!(url); + final MockHttpFetchResponse? response = + await mockHttpFetchResponseFactory!(url); if (response != null) { return response; } @@ -1693,8 +1696,7 @@ class MockHttpFetchPayload implements HttpFetchPayload { while (currentIndex < totalLength) { final int chunkSize = math.min(_chunkSize, totalLength - currentIndex); final Uint8List chunk = Uint8List.sublistView( - _byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize - ); + _byteBuffer.asByteData(), currentIndex, currentIndex + chunkSize); callback(chunk.toJS as T); currentIndex += chunkSize; } @@ -1704,10 +1706,12 @@ class MockHttpFetchPayload implements HttpFetchPayload { Future asByteBuffer() async => _byteBuffer; @override - Future json() async => throw AssertionError('json not supported by mock'); + Future json() async => + throw AssertionError('json not supported by mock'); @override - Future text() async => throw AssertionError('text not supported by mock'); + Future text() async => + throw AssertionError('text not supported by mock'); } /// Indicates a missing HTTP payload when one was expected, such as when @@ -2762,7 +2766,8 @@ extension DomOffscreenCanvasExtension on DomOffscreenCanvas { @JS('transferToImageBitmap') external JSAny? _transferToImageBitmap(); - DomImageBitmap transferToImageBitmap() => _transferToImageBitmap()! as DomImageBitmap; + DomImageBitmap transferToImageBitmap() => + _transferToImageBitmap()! as DomImageBitmap; } DomOffscreenCanvas createDomOffscreenCanvas(int width, int height) => @@ -3331,8 +3336,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); } } @@ -3469,10 +3474,8 @@ external JSAny? get _finalizationRegistryConstructor; // dart2js that causes a crash in the Google3 build if we do use a factory // constructor. See b/284478971 DomFinalizationRegistry createDomFinalizationRegistry(JSFunction cleanup) => - js_util.callConstructor( - _finalizationRegistryConstructor!.toObjectShallow, - [cleanup] - ); + js_util.callConstructor( + _finalizationRegistryConstructor!.toObjectShallow, [cleanup]); extension DomFinalizationRegistryExtension on DomFinalizationRegistry { @JS('register') @@ -3481,11 +3484,12 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry { @JS('register') external JSVoid _register2(JSAny target, JSAny value, JSAny token); void register(Object target, Object value, [Object? token]) { - if (token != null) { - _register2(target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow); - } else { - _register1(target.toJSAnyShallow, value.toJSAnyShallow); - } + if (token != null) { + _register2( + target.toJSAnyShallow, value.toJSAnyShallow, token.toJSAnyShallow); + } else { + _register1(target.toJSAnyShallow, value.toJSAnyShallow); + } } @JS('unregister') @@ -3496,3 +3500,8 @@ 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; From 6428d4cbf403e40bdbcedab49d76d3f92d0ff3bb Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 15 Jun 2023 11:09:52 -0700 Subject: [PATCH 12/30] Print useragent if OffscreenCanvas is not available --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 00ab0a00f25ae..9eafa17adb655 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -241,6 +241,7 @@ class Surface { _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); if (!browserSupportsOffscreenCanvas) { + print(domWindow.navigator.userAgent); throw Exception('OffscreenCanvas is not supported'); } final DomOffscreenCanvas htmlCanvas = createDomOffscreenCanvas( From f0ce392c45c4dda798783d56d3c935f7d16f5231 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 15 Jun 2023 14:12:04 -0700 Subject: [PATCH 13/30] Fall back to HTML Canvas on Safari --- .../lib/src/engine/canvaskit/rasterizer.dart | 18 +- .../lib/src/engine/canvaskit/surface.dart | 157 +++++++++++++----- lib/web_ui/lib/src/engine/dom.dart | 5 + lib/web_ui/test/canvaskit/surface_test.dart | 32 ++-- 4 files changed, 141 insertions(+), 71 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index e92594cd72ced..7e8539443fe1d 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -15,21 +15,11 @@ class Rasterizer { /// used to render to many RenderCanvases to produce the rendered scene. final Surface _offscreenSurface = Surface(); ui.Size _currentFrameSize = ui.Size.zero; - late SurfaceFrame _currentFrame; /// Render the given [picture] so it is displayed by the given [canvas]. - void rasterizeToCanvas(RenderCanvas canvas, CkPicture picture) { - // Ensure the [canvas] is the correct size. - canvas.ensureSize(_currentFrameSize); - - final CkCanvas skCanvas = _currentFrame.skiaCanvas; - skCanvas.clear(const ui.Color(0x00000000)); - skCanvas.drawPicture(picture); - _currentFrame.submit(); - - final DomImageBitmap bitmap = - _offscreenSurface.offscreenCanvas!.transferToImageBitmap(); - canvas.renderContext!.transferFromImageBitmap(bitmap); + Future rasterizeToCanvas(RenderCanvas canvas, CkPicture picture) async { + await _offscreenSurface.rasterizeToCanvas( + _currentFrameSize, canvas, picture); } /// Sets the maximum size of the Skia resource cache, in bytes. @@ -46,7 +36,7 @@ class Rasterizer { } _currentFrameSize = layerTree.frameSize; - _currentFrame = _offscreenSurface.acquireFrame(_currentFrameSize); + _offscreenSurface.acquireFrame(_currentFrameSize); RenderCanvasFactory.instance.baseCanvas.ensureSize(_currentFrameSize); HtmlViewEmbedder.instance.frameSize = _currentFrameSize; final CkPictureRecorder pictureRecorder = CkPictureRecorder(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 9eafa17adb655..badaa9c86371a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -14,6 +14,8 @@ import '../util.dart'; import '../window.dart'; import 'canvas.dart'; import 'canvaskit_api.dart'; +import 'picture.dart'; +import 'render_canvas.dart'; import 'util.dart'; // Only supported in profile/release mode. Allows Flutter to use MSAA but @@ -79,8 +81,16 @@ class Surface { int? _glContext; int? _skiaCacheBytes; - /// The underlying `` element used for this surface. - DomOffscreenCanvas? offscreenCanvas; + /// 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; + int _pixelWidth = -1; int _pixelHeight = -1; int _sampleCount = -1; @@ -98,6 +108,24 @@ class Surface { } } + Future rasterizeToCanvas( + ui.Size frameSize, RenderCanvas canvas, CkPicture picture) async { + canvas.ensureSize(frameSize); + + final CkCanvas skCanvas = _surface!.getCanvas(); + skCanvas.clear(const ui.Color(0x00000000)); + skCanvas.drawPicture(picture); + _surface!.flush(); + + DomImageBitmap bitmap; + if (_offscreenCanvasSupported) { + bitmap = _offscreenCanvas!.transferToImageBitmap(); + } else { + bitmap = (await createImageBitmap(_canvasElement!))!; + } + canvas.renderContext!.transferFromImageBitmap(bitmap); + } + /// Acquire a frame of the given [size] containing a drawable canvas. /// /// The given [size] is in physical pixels. @@ -168,8 +196,13 @@ class Surface { final ui.Size newSize = size * 1.4; _surface?.dispose(); _surface = null; - offscreenCanvas!.width = newSize.width; - offscreenCanvas!.height = newSize.height; + if (_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(); @@ -208,7 +241,7 @@ class Surface { } JSVoid _contextLostListener(DomEvent event) { - assert(event.target == offscreenCanvas, + assert(event.target == _offscreenCanvas || event.target == _canvasElement, 'Received a context lost event for a disposed canvas'); _contextLost = true; _forceNewContext = true; @@ -220,18 +253,32 @@ class Surface { /// It's better to reuse canvas if possible. void _createNewCanvas(ui.Size physicalSize) { // Clear the container, if it's not empty. We're going to create a new . - if (offscreenCanvas != null) { - offscreenCanvas!.removeEventListener( + 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, ); - offscreenCanvas!.removeEventListener( + _canvasElement!.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false, ); - offscreenCanvas = null; + _canvasElement = null; _cachedContextRestoredListener = null; _cachedContextLostListener = null; } @@ -240,15 +287,22 @@ class Surface { // we ensure that the rendred picture covers the entire browser window. _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); - if (!browserSupportsOffscreenCanvas) { - print(domWindow.navigator.userAgent); - throw Exception('OffscreenCanvas is not supported'); + DomEventTarget htmlCanvas; + if (_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 DomOffscreenCanvas htmlCanvas = createDomOffscreenCanvas( - _pixelWidth, - _pixelHeight, - ); - offscreenCanvas = htmlCanvas; // 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. @@ -273,15 +327,24 @@ class Surface { _contextLost = false; if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) { - final int glContext = canvasKit.GetOffscreenWebGLContext( - htmlCanvas, - SkWebGLContextOptions( - // Default to no anti-aliasing. Paint commands can be explicitly - // anti-aliased by setting their `Paint` object's `antialias` property. - antialias: _kUsingMSAA ? 1 : 0, - majorVersion: webGLVersion.toDouble(), - ), - ).toInt(); + int glContext = 0; + final SkWebGLContextOptions options = SkWebGLContextOptions( + // Default to no anti-aliasing. Paint commands can be explicitly + // anti-aliased by setting their `Paint` object's `antialias` property. + antialias: _kUsingMSAA ? 1 : 0, + majorVersion: webGLVersion.toDouble(), + ); + if (_offscreenCanvasSupported) { + glContext = canvasKit.GetOffscreenWebGLContext( + _offscreenCanvas!, + options, + ).toInt(); + } else { + glContext = canvasKit.GetWebGLContext( + _canvasElement!, + options, + ).toInt(); + } _glContext = glContext; @@ -302,22 +365,24 @@ class Surface { } void _initWebglParams() { - final WebGLContext gl = offscreenCanvas!.getGlContext(webGLVersion); + WebGLContext gl; + if (_offscreenCanvasSupported) { + gl = _offscreenCanvas!.getGlContext(webGLVersion); + } else { + gl = _canvasElement!.getGlContext(webGLVersion); + } _sampleCount = gl.getParameter(gl.samples); _stencilBits = gl.getParameter(gl.stencilBits); } CkSurface _createNewSurface(ui.Size size) { - assert(offscreenCanvas != null); + assert(_offscreenCanvas != null || _canvasElement != null); if (webGLVersion == -1) { - return _makeSoftwareCanvasSurface( - offscreenCanvas!, 'WebGL support not detected'); + return _makeSoftwareCanvasSurface('WebGL support not detected'); } else if (configuration.canvasKitForceCpuOnly) { - return _makeSoftwareCanvasSurface( - offscreenCanvas!, 'CPU rendering forced by application'); + return _makeSoftwareCanvasSurface('CPU rendering forced by application'); } else if (_glContext == 0) { - return _makeSoftwareCanvasSurface( - offscreenCanvas!, 'Failed to initialize WebGL context'); + return _makeSoftwareCanvasSurface('Failed to initialize WebGL context'); } else { final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface( _grContext!, @@ -328,8 +393,7 @@ class Surface { _stencilBits); if (skSurface == null) { - return _makeSoftwareCanvasSurface( - offscreenCanvas!, 'Failed to initialize WebGL surface'); + return _makeSoftwareCanvasSurface('Failed to initialize WebGL surface'); } return CkSurface(skSurface, _glContext); @@ -338,14 +402,20 @@ class Surface { static bool _didWarnAboutWebGlInitializationFailure = false; - CkSurface _makeSoftwareCanvasSurface( - DomOffscreenCanvas htmlCanvas, String reason) { + CkSurface _makeSoftwareCanvasSurface(String reason) { if (!_didWarnAboutWebGlInitializationFailure) { printWarning('WARNING: Falling back to CPU-only rendering. $reason.'); _didWarnAboutWebGlInitializationFailure = true; } + + SkSurface surface; + if (_offscreenCanvasSupported) { + surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!); + } else { + surface = canvasKit.MakeSWCanvasSurface(_canvasElement!); + } return CkSurface( - canvasKit.MakeOffscreenSWCanvasSurface(htmlCanvas), + surface, null, ); } @@ -356,14 +426,19 @@ class Surface { } void dispose() { - offscreenCanvas?.removeEventListener( + _offscreenCanvas?.removeEventListener( 'webglcontextlost', _cachedContextLostListener, false); - offscreenCanvas?.removeEventListener( + _offscreenCanvas?.removeEventListener( 'webglcontextrestored', _cachedContextRestoredListener, false); _cachedContextLostListener = null; _cachedContextRestoredListener = null; _surface?.dispose(); } + + /// Safari 15 doesn't support OffscreenCanvas at all. Safari 16 supports + /// OffscreenCanvas, but only with the context2d API, not WebGL. + bool get _offscreenCanvasSupported => + browserSupportsOffscreenCanvas && !isSafari; } /// A Dart wrapper around Skia's CkSurface. diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 6123b2fa73143..e70a0fec21633 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -190,6 +190,11 @@ external DomIntl get domIntl; @JS('Symbol') external DomSymbol get domSymbol; +@JS('createImageBitmap') +external JSPromise _createImageBitmap(DomCanvasElement canvas); +Future createImageBitmap(DomCanvasElement canvas) => + js_util.promiseToFuture(_createImageBitmap(canvas)); + @JS() @staticInterop class DomNavigator {} diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index 4ef9c5c49b754..a98cfbf0d41e0 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -26,7 +26,7 @@ void testMain() { final Surface surface = Surface(); final CkSurface originalSurface = surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; - final DomOffscreenCanvas original = surface.offscreenCanvas!; + final DomOffscreenCanvas original = surface.debugOffscreenCanvas!; // Expect exact requested dimensions. expect(original.width, 9); @@ -38,7 +38,7 @@ void testMain() { // Skia renders into the visible area. final CkSurface shrunkSurface = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomOffscreenCanvas shrunk = surface.offscreenCanvas!; + final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!; expect(shrunk, same(original)); expect(shrunkSurface, isNot(same(originalSurface))); expect(shrunkSurface.width(), 5); @@ -48,7 +48,7 @@ void testMain() { // by 40% to accommodate future increases. final CkSurface firstIncreaseSurface = surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; - final DomOffscreenCanvas firstIncrease = surface.offscreenCanvas!; + final DomOffscreenCanvas firstIncrease = surface.debugOffscreenCanvas!; expect(firstIncrease, same(original)); expect(firstIncreaseSurface, isNot(same(shrunkSurface))); @@ -61,7 +61,7 @@ void testMain() { // Subsequent increases within 40% reuse the old canvas. final CkSurface secondIncreaseSurface = surface.acquireFrame(const ui.Size(11, 22)).skiaSurface; - final DomOffscreenCanvas secondIncrease = surface.offscreenCanvas!; + final DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!; expect(secondIncrease, same(firstIncrease)); expect(secondIncreaseSurface, isNot(same(firstIncreaseSurface))); expect(secondIncreaseSurface.width(), 11); @@ -69,7 +69,7 @@ void testMain() { // 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.offscreenCanvas!; + final DomOffscreenCanvas huge = surface.debugOffscreenCanvas!; expect(huge, same(secondIncrease)); expect(hugeSurface, isNot(same(secondIncreaseSurface))); @@ -82,7 +82,7 @@ void testMain() { // Shrink again. Reuse the last allocated surface. final CkSurface shrunkSurface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomOffscreenCanvas shrunk2 = surface.offscreenCanvas!; + final DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!; expect(shrunk2, same(huge)); expect(shrunkSurface2, isNot(same(hugeSurface))); expect(shrunkSurface2.width(), 5); @@ -93,7 +93,7 @@ void testMain() { window.debugOverrideDevicePixelRatio(2.0); final CkSurface dpr2Surface2 = surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; - final DomOffscreenCanvas dpr2Canvas = surface.offscreenCanvas!; + final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!; expect(dpr2Canvas, same(huge)); expect(dpr2Surface2, isNot(same(hugeSurface))); expect(dpr2Surface2.width(), 5); @@ -123,7 +123,7 @@ void testMain() { expect(afterAcquireFrame, same(before)); // Emulate WebGL context loss. - final DomOffscreenCanvas canvas = surface.offscreenCanvas!; + final DomOffscreenCanvas canvas = surface.debugOffscreenCanvas!; final Object ctx = canvas.getContext('webgl2')!; final Object loseContextExtension = js_util.callMethod( ctx, @@ -163,8 +163,8 @@ void testMain() { expect(original.width(), 10); expect(original.height(), 16); - expect(surface.offscreenCanvas!.width, 10); - expect(surface.offscreenCanvas!.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. @@ -173,8 +173,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(highDpr.width(), 10); expect(highDpr.height(), 16); - expect(surface.offscreenCanvas!.width, 10); - expect(surface.offscreenCanvas!.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. @@ -183,8 +183,8 @@ void testMain() { surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; expect(lowDpr.width(), 10); expect(lowDpr.height(), 16); - expect(surface.offscreenCanvas!.width, 10); - expect(surface.offscreenCanvas!.height, 16); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); @@ -192,8 +192,8 @@ void testMain() { surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; expect(changeRatioAndSize.width(), 10); expect(changeRatioAndSize.height(), 16); - expect(surface.offscreenCanvas!.width, 10); - expect(surface.offscreenCanvas!.height, 16); + expect(surface.debugOffscreenCanvas!.width, 10); + expect(surface.debugOffscreenCanvas!.height, 16); }); }); } From 2d76c46cea8f05f939decb716cb0ba173aed1d7c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 15 Jun 2023 14:16:40 -0700 Subject: [PATCH 14/30] Don't test OffscreenCanvas when it's not supported --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 14 +++++++------- lib/web_ui/test/canvaskit/surface_test.dart | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index badaa9c86371a..019890d00e3f6 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -118,7 +118,7 @@ class Surface { _surface!.flush(); DomImageBitmap bitmap; - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { bitmap = _offscreenCanvas!.transferToImageBitmap(); } else { bitmap = (await createImageBitmap(_canvasElement!))!; @@ -196,7 +196,7 @@ class Surface { final ui.Size newSize = size * 1.4; _surface?.dispose(); _surface = null; - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { _offscreenCanvas!.width = newSize.width; _offscreenCanvas!.height = newSize.height; } else { @@ -288,7 +288,7 @@ class Surface { _pixelWidth = physicalSize.width.ceil(); _pixelHeight = physicalSize.height.ceil(); DomEventTarget htmlCanvas; - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas( _pixelWidth, _pixelHeight, @@ -334,7 +334,7 @@ class Surface { antialias: _kUsingMSAA ? 1 : 0, majorVersion: webGLVersion.toDouble(), ); - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { glContext = canvasKit.GetOffscreenWebGLContext( _offscreenCanvas!, options, @@ -366,7 +366,7 @@ class Surface { void _initWebglParams() { WebGLContext gl; - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { gl = _offscreenCanvas!.getGlContext(webGLVersion); } else { gl = _canvasElement!.getGlContext(webGLVersion); @@ -409,7 +409,7 @@ class Surface { } SkSurface surface; - if (_offscreenCanvasSupported) { + if (Surface.offscreenCanvasSupported) { surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!); } else { surface = canvasKit.MakeSWCanvasSurface(_canvasElement!); @@ -437,7 +437,7 @@ class Surface { /// Safari 15 doesn't support OffscreenCanvas at all. Safari 16 supports /// OffscreenCanvas, but only with the context2d API, not WebGL. - bool get _offscreenCanvasSupported => + static bool get offscreenCanvasSupported => browserSupportsOffscreenCanvas && !isSafari; } diff --git a/lib/web_ui/test/canvaskit/surface_test.dart b/lib/web_ui/test/canvaskit/surface_test.dart index a98cfbf0d41e0..f01016db5ae16 100644 --- a/lib/web_ui/test/canvaskit/surface_test.dart +++ b/lib/web_ui/test/canvaskit/surface_test.dart @@ -104,7 +104,7 @@ void testMain() { // which cannot be a different size from the canvas. // TODO(hterkelsen): See if we can give a custom size for software // surfaces. - }, skip: isFirefox); + }, skip: isFirefox || !Surface.offscreenCanvasSupported); test( 'Surface creates new context when WebGL context is restored', @@ -152,7 +152,7 @@ void testMain() { expect(afterContextLost, isNot(same(before))); }, // Firefox can't create a WebGL2 context in headless mode. - skip: isFirefox, + skip: isFirefox || !Surface.offscreenCanvasSupported, ); // Regression test for https://github.com/flutter/flutter/issues/75286 @@ -194,6 +194,8 @@ void testMain() { expect(changeRatioAndSize.height(), 16); expect(surface.debugOffscreenCanvas!.width, 10); expect(surface.debugOffscreenCanvas!.height, 16); - }); + }, + skip: !Surface.offscreenCanvasSupported, + ); }); } From bcc212245fc672e2b7d3a702a4bdb83468bd15f5 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 22 Jun 2023 13:03:28 -0700 Subject: [PATCH 15/30] Use createImageBitmap to crop the bitmap --- .../src/engine/canvaskit/render_canvas.dart | 25 +++++++++---------- .../lib/src/engine/canvaskit/surface.dart | 16 ++++++++++-- lib/web_ui/lib/src/engine/dom.dart | 16 ++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) 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 86d62327d7fbd..a5ca7c2584733 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -118,13 +118,13 @@ class RenderCanvas { throw CanvasKitError('Cannot create canvases of empty size.'); } - // Check if the window is the same size as before, and if so, don't allocate + // Check if the frame is the same size as before, and if so, don't allocate // a new canvas as the previous canvas is big enough to fit everything. final ui.Size? previousRenderSize = _currentRenderSize; if (previousRenderSize != null && size.width == previousRenderSize.width && size.height == previousRenderSize.height) { - // The existing surface is still reusable. + // The existing canvas doesn't need to be resized. if (window.devicePixelRatio != _currentDevicePixelRatio) { _updateLogicalHtmlCanvasSize(); } @@ -132,17 +132,16 @@ class RenderCanvas { } final ui.Size? previousCanvasSize = _currentCanvasPhysicalSize; - // Initialize a new, larger, canvas. If the size is growing, then make the - // new canvas larger than required to avoid many canvas creations. - if (previousCanvasSize != null && - (size.width > previousCanvasSize.width || - size.height > previousCanvasSize.height)) { - final ui.Size newSize = size * 1.4; - canvasElement!.width = newSize.width; - canvasElement!.height = newSize.height; - _currentCanvasPhysicalSize = newSize; - _pixelWidth = newSize.width.ceil(); - _pixelHeight = newSize.height.ceil(); + // 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. + if (previousCanvasSize != null) { + canvasElement!.width = size.width; + canvasElement!.height = size.height; + _currentCanvasPhysicalSize = size; + _pixelWidth = size.width.ceil(); + _pixelHeight = size.height.ceil(); _updateLogicalHtmlCanvasSize(); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 019890d00e3f6..ab43a2f9c9c39 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -119,9 +119,21 @@ class Surface { DomImageBitmap bitmap; if (Surface.offscreenCanvasSupported) { - bitmap = _offscreenCanvas!.transferToImageBitmap(); + bitmap = (await createSizedOffscreenImageBitmap( + _offscreenCanvas!, + 0, + _pixelHeight - frameSize.height.toInt(), + frameSize.width.toInt(), + frameSize.height.toInt(), + ))!; } else { - bitmap = (await createImageBitmap(_canvasElement!))!; + bitmap = (await createSizedImageBitmap( + _canvasElement!, + 0, + _pixelHeight - frameSize.height.toInt(), + frameSize.width.toInt(), + frameSize.height.toInt(), + ))!; } canvas.renderContext!.transferFromImageBitmap(bitmap); } diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index e70a0fec21633..041103cabc21c 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -195,6 +195,22 @@ external JSPromise _createImageBitmap(DomCanvasElement canvas); Future createImageBitmap(DomCanvasElement canvas) => js_util.promiseToFuture(_createImageBitmap(canvas)); +@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 _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 {} From 084732d10db3fdb400c776f8250478b3fb9b5bf6 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 22 Jun 2023 13:13:49 -0700 Subject: [PATCH 16/30] Fix test --- lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a5ca7c2584733..df6cadacaf630 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -137,8 +137,8 @@ class RenderCanvas { // because then when we call `transferFromImageBitmap()` the bitmap will // be scaled to cover the entire canvas. if (previousCanvasSize != null) { - canvasElement!.width = size.width; - canvasElement!.height = size.height; + canvasElement!.width = size.width.ceilToDouble(); + canvasElement!.height = size.height.ceilToDouble(); _currentCanvasPhysicalSize = size; _pixelWidth = size.width.ceil(); _pixelHeight = size.height.ceil(); From 901bb70c05980169afb673164647cd92bf623978 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 22 Jun 2023 13:43:34 -0700 Subject: [PATCH 17/30] improve doc comment --- lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 df6cadacaf630..9b3f3d7408528 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -118,8 +118,8 @@ class RenderCanvas { throw CanvasKitError('Cannot create canvases of empty size.'); } - // Check if the frame is the same size as before, and if so, don't allocate - // a new canvas as the previous canvas is big enough to fit everything. + // Check if the frame is the same size as before, and if so, we don't need + // to resize the canvas. final ui.Size? previousRenderSize = _currentRenderSize; if (previousRenderSize != null && size.width == previousRenderSize.width && From 4c08850ca29bcdea2cb36588b9198594e60da3b3 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 2 Aug 2023 16:16:23 -0700 Subject: [PATCH 18/30] Fix bad merge --- lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2b4844b0f6992..52206b05cd126 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -165,8 +165,8 @@ extension CanvasKitExtension on CanvasKit { external JSNumber _GetOffscreenWebGLContext( DomOffscreenCanvas canvas, SkWebGLContextOptions options); double GetOffscreenWebGLContext( - DomOffscreenCanvas canvas, SkWebGLContextOptions options) => - _GetOffscreenWebGLContext(canvas, options).toDart; + DomOffscreenCanvas canvas, SkWebGLContextOptions options) => + _GetOffscreenWebGLContext(canvas, options).toDartDouble; @JS('MakeGrContext') external SkGrContext _MakeGrContext(JSNumber glContext); From 994b80b22fc9a4db3aea18ac3b84b74461811f43 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 2 Aug 2023 16:30:21 -0700 Subject: [PATCH 19/30] Remove unused import --- lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index b1373082414a0..1a9ad455c308a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -15,7 +15,6 @@ import '../window.dart'; import 'canvas.dart'; import 'embedded_views_diff.dart'; import 'path.dart'; -import 'picture.dart'; import 'picture_recorder.dart'; import 'render_canvas.dart'; import 'render_canvas_factory.dart'; From d0e400924f99d37c8416fe6e1d598f8f3ea27260 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 29 Aug 2023 12:36:19 -0700 Subject: [PATCH 20/30] Fix merge --- lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9b3f3d7408528..91b1310cb16f1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -45,7 +45,7 @@ class RenderCanvas { int _pixelWidth = -1; int _pixelHeight = -1; - DomCanvasBitmapRendererContext? renderContext; + DomCanvasRenderingContextBitmapRenderer? renderContext; ui.Size? _currentCanvasPhysicalSize; ui.Size? _currentRenderSize; @@ -94,7 +94,7 @@ class RenderCanvas { height: _pixelHeight, ); canvasElement = htmlCanvas; - renderContext = htmlCanvas.bitmapRendererContext; + renderContext = htmlCanvas.contextBitmapRenderer; // 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 From a09090f7d716ff6d8f7a6c459dc2f3a75330439e Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 29 Aug 2023 16:31:18 -0700 Subject: [PATCH 21/30] Add back maximum overlays and fix tests --- .../src/engine/canvaskit/embedded_views.dart | 68 ++++++++++++------- .../lib/src/engine/canvaskit/rasterizer.dart | 9 +-- .../lib/src/engine/canvaskit/surface.dart | 4 +- .../test/canvaskit/embedded_views_test.dart | 24 +------ 4 files changed, 50 insertions(+), 55 deletions(-) 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 1a9ad455c308a..18e0850a16a16 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -15,6 +15,7 @@ import '../window.dart'; import 'canvas.dart'; import 'embedded_views_diff.dart'; import 'path.dart'; +import 'picture.dart'; import 'picture_recorder.dart'; import 'render_canvas.dart'; import 'render_canvas_factory.dart'; @@ -48,6 +49,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 = {}; @@ -359,22 +364,28 @@ class HtmlViewEmbedder { (_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty) ? null : diffViewList(_activeCompositionOrder, _compositionOrder); - _updateOverlays(diffResult); + final List? overlayGroups = _updateOverlays(diffResult); assert( - _context.pictureRecorders.length == _overlays.length, - 'There should be the same number of picture recorders ' + _context.pictureRecorders.length >= _overlays.length, + 'There should at least as many picture recorders ' '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).', ); int pictureRecorderIndex = 0; - for (int i = 0; i < _compositionOrder.length; i++) { - final int viewId = _compositionOrder[i]; - if (_overlays[viewId] != null) { - CanvasKitRenderer.instance.rasterizer.rasterizeToCanvas( - _overlays[viewId]!, - _context.pictureRecorders[pictureRecorderIndex].endRecording()); - pictureRecorderIndex++; + if (overlayGroups != null) { + for (final OverlayGroup overlayGroup in overlayGroups) { + final RenderCanvas overlay = _overlays[overlayGroup.last]!; + final List pictures = []; + for (int i = 0; i < overlayGroup.visibleCount; i++) { + pictures.add( + _context.pictureRecorders[pictureRecorderIndex].endRecording()); + pictureRecorderIndex++; + } + CanvasKitRenderer.instance.rasterizer + .rasterizeToCanvas(overlay, pictures); } + } else { + // The overlay groups are null, so it's the same as before. } for (final CkPictureRecorder recorder in _context.pictureRecordersCreatedDuringPreroll) { @@ -537,13 +548,13 @@ class HtmlViewEmbedder { // composition order of the current and previous frame, respectively. // // TODO(hterkelsen): Test this more thoroughly. - void _updateOverlays(ViewListDiffResult? diffResult) { + List? _updateOverlays(ViewListDiffResult? diffResult) { if (diffResult != null && diffResult.viewsToAdd.isEmpty && diffResult.viewsToRemove.isEmpty) { // The composition order has not changed, continue using the assigned // overlays. - return; + return null; } // Group platform views from their composition order. // Each group contains one visible view, and any number of invisible views @@ -574,6 +585,7 @@ class HtmlViewEmbedder { .forEach(_initializeOverlay); } assert(_overlays.length == viewsNeedingOverlays.length); + return overlayGroups; } // Group the platform views into "overlay groups". These are sublists @@ -586,7 +598,7 @@ class HtmlViewEmbedder { // the rest of the scene. List getOverlayGroups(List views) { final List result = []; - OverlayGroup currentGroup = OverlayGroup([]); + OverlayGroup currentGroup = OverlayGroup(); for (int i = 0; i < views.length; i++) { final int view = views[i]; @@ -595,8 +607,10 @@ class HtmlViewEmbedder { currentGroup.add(view); } else { // `view` is visible. - if (!currentGroup.hasVisibleView) { - // If `view` is the first visible one of the group, add it. + if (!currentGroup.hasVisibleView || + result.length + 1 >= HtmlViewEmbedder.maximumOverlays) { + // If `view` is the first visible one of the group or we've reached + // the maximum number of overlays, add it. currentGroup.add(view, visible: true); } else { // There's already a visible `view` in `currentGroup`, so a new @@ -606,7 +620,8 @@ class HtmlViewEmbedder { // We only care about groups that have one visible view. result.add(currentGroup); } - currentGroup = OverlayGroup([view], visible: true); + currentGroup = OverlayGroup(); + currentGroup.add(view, visible: true); } } } @@ -667,29 +682,30 @@ class HtmlViewEmbedder { /// Every overlay group is a list containing a visible view preceded or followed /// by zero or more invisible views. class OverlayGroup { - /// Constructor - OverlayGroup( - List viewGroup, { - bool visible = false, - }) : _group = viewGroup, - _containsVisibleView = visible; + OverlayGroup() : _group = []; // The internal list of ints. final List _group; - // A boolean flag to mark if any visible view has been added to the list. - bool _containsVisibleView; + + /// The number of visible views in this group. + int _visibleCount = 0; /// Add a [view] (maybe [visible]) to this group. void add(int view, {bool visible = false}) { _group.add(view); - _containsVisibleView |= visible; + if (visible) { + _visibleCount++; + } } /// Get the "last" view added to this group. int get last => _group.last; /// Returns true if this group contains any visible view. - bool get hasVisibleView => _group.isNotEmpty && _containsVisibleView; + bool get hasVisibleView => _visibleCount > 0; + + /// Returns the number of visible views in this overlay group. + int get visibleCount => _visibleCount; } /// Represents a Clip Chain (for a view). diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 7e8539443fe1d..32de0f7150420 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -16,10 +16,11 @@ class Rasterizer { final Surface _offscreenSurface = Surface(); ui.Size _currentFrameSize = ui.Size.zero; - /// Render the given [picture] so it is displayed by the given [canvas]. - Future rasterizeToCanvas(RenderCanvas canvas, CkPicture picture) async { + /// Render the given [pictures] so it is displayed by the given [canvas]. + Future rasterizeToCanvas( + RenderCanvas canvas, List pictures) async { await _offscreenSurface.rasterizeToCanvas( - _currentFrameSize, canvas, picture); + _currentFrameSize, canvas, pictures); } /// Sets the maximum size of the Skia resource cache, in bytes. @@ -49,7 +50,7 @@ class Rasterizer { RenderCanvasFactory.instance.baseCanvas.addToScene(); rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, - pictureRecorder.endRecording()); + [pictureRecorder.endRecording()]); HtmlViewEmbedder.instance.submitFrame(); } finally { diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index ab43a2f9c9c39..95de110f7f11c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -109,12 +109,12 @@ class Surface { } Future rasterizeToCanvas( - ui.Size frameSize, RenderCanvas canvas, CkPicture picture) async { + ui.Size frameSize, RenderCanvas canvas, List pictures) async { canvas.ensureSize(frameSize); final CkCanvas skCanvas = _surface!.getCanvas(); skCanvas.clear(const ui.Color(0x00000000)); - skCanvas.drawPicture(picture); + pictures.forEach(skCanvas.drawPicture); _surface!.flush(); DomImageBitmap bitmap; diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index cc4f04fe57a91..49db4e0dddc63 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -337,7 +337,6 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -371,7 +370,7 @@ void testMain() { ]); // Frame 4: - // Render: more platform views than max cache size. + // Render: more platform views than max overlay count. // Expect: main canvas, backup overlay, maximum overlays. await Future.delayed(Duration.zero); renderTestScene(viewCount: 16); @@ -390,23 +389,14 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -500,11 +490,8 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -529,11 +516,8 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -558,11 +542,8 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); @@ -587,11 +568,8 @@ void testMain() { _platformView, _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, - _overlay, _platformView, _overlay, ]); From 876a42425f7e541016676fa0ff1ec0bcabe24c04 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 30 Aug 2023 11:06:50 -0700 Subject: [PATCH 22/30] Handle case where platform views don't change --- .../src/engine/canvaskit/embedded_views.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 18e0850a16a16..bd559243f2396 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -65,6 +65,9 @@ class HtmlViewEmbedder { /// The most recent composition order. final List _activeCompositionOrder = []; + /// The most recent overlay groups. + List _activeOverlayGroups = []; + /// The size of the frame, in physical pixels. ui.Size _frameSize = ui.window.physicalSize; @@ -365,6 +368,9 @@ class HtmlViewEmbedder { ? null : diffViewList(_activeCompositionOrder, _compositionOrder); final List? overlayGroups = _updateOverlays(diffResult); + if (overlayGroups != null) { + _activeOverlayGroups = overlayGroups; + } assert( _context.pictureRecorders.length >= _overlays.length, 'There should at least as many picture recorders ' @@ -372,20 +378,16 @@ class HtmlViewEmbedder { ); int pictureRecorderIndex = 0; - if (overlayGroups != null) { - for (final OverlayGroup overlayGroup in overlayGroups) { - final RenderCanvas overlay = _overlays[overlayGroup.last]!; - final List pictures = []; - for (int i = 0; i < overlayGroup.visibleCount; i++) { - pictures.add( - _context.pictureRecorders[pictureRecorderIndex].endRecording()); - pictureRecorderIndex++; - } - CanvasKitRenderer.instance.rasterizer - .rasterizeToCanvas(overlay, pictures); + for (final OverlayGroup overlayGroup in _activeOverlayGroups) { + final RenderCanvas overlay = _overlays[overlayGroup.last]!; + final List pictures = []; + for (int i = 0; i < overlayGroup.visibleCount; i++) { + pictures.add( + _context.pictureRecorders[pictureRecorderIndex].endRecording()); + pictureRecorderIndex++; } - } else { - // The overlay groups are null, so it's the same as before. + CanvasKitRenderer.instance.rasterizer + .rasterizeToCanvas(overlay, pictures); } for (final CkPictureRecorder recorder in _context.pictureRecordersCreatedDuringPreroll) { From b4d838353df589f809aa6daebf4e7a17453d7102 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 30 Aug 2023 13:49:28 -0700 Subject: [PATCH 23/30] Fix bad merge --- lib/web_ui/lib/src/engine/dom.dart | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index f16ae49670142..26e5558f75e5d 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -1455,32 +1455,6 @@ extension DomImageBitmapExtension on DomImageBitmap { } -@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 {} From eab0290d09fd5a256c48eb87447bd00030869243 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 6 Sep 2023 14:44:48 -0700 Subject: [PATCH 24/30] Respond to comments --- .../src/engine/canvaskit/embedded_views.dart | 3 +- .../lib/src/engine/canvaskit/rasterizer.dart | 4 +- .../src/engine/canvaskit/render_canvas.dart | 112 +++++------------- .../lib/src/engine/canvaskit/surface.dart | 4 +- .../canvaskit/render_canvas_factory_test.dart | 10 +- .../test/canvaskit/render_canvas_test.dart | 8 +- 6 files changed, 44 insertions(+), 97 deletions(-) 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 bd559243f2396..06c6f5aa6a3fa 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -373,7 +373,7 @@ class HtmlViewEmbedder { } assert( _context.pictureRecorders.length >= _overlays.length, - 'There should at least as many picture recorders ' + 'There should be at least as many picture recorders ' '(${_context.pictureRecorders.length}) as overlays (${_overlays.length}).', ); @@ -639,7 +639,6 @@ class HtmlViewEmbedder { // Try reusing a cached overlay created for another platform view. final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas(); - overlay.ensureSize(_frameSize); _overlays[viewId] = overlay; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 32de0f7150420..8d150ecae4144 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -38,7 +38,6 @@ class Rasterizer { _currentFrameSize = layerTree.frameSize; _offscreenSurface.acquireFrame(_currentFrameSize); - RenderCanvasFactory.instance.baseCanvas.ensureSize(_currentFrameSize); HtmlViewEmbedder.instance.frameSize = _currentFrameSize; final CkPictureRecorder pictureRecorder = CkPictureRecorder(); pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize); @@ -48,7 +47,8 @@ class Rasterizer { compositorFrame.raster(layerTree, ignoreRasterCache: true); - RenderCanvasFactory.instance.baseCanvas.addToScene(); + CanvasKitRenderer.instance.sceneHost! + .prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement); rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas, [pictureRecorder.endRecording()]); 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 91b1310cb16f1..2021985ff9505 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -2,6 +2,8 @@ // 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'; @@ -27,7 +29,12 @@ import 'util.dart'; /// a single OffscreenCanvas and multiple RenderCanvases allows us to only /// create a single WebGL context. class RenderCanvas { - RenderCanvas(); + RenderCanvas() { + canvasElement.setAttribute('aria-hidden', 'true'); + canvasElement.style.position = 'absolute'; + _updateLogicalHtmlCanvasSize(); + htmlElement.append(canvasElement); + } /// The root HTML element for this canvas. /// @@ -41,24 +48,15 @@ class RenderCanvas { final DomElement htmlElement = createDomElement('flt-canvas-container'); /// The underlying `` element used to display the pixels. - DomCanvasElement? canvasElement; - int _pixelWidth = -1; - int _pixelHeight = -1; + final DomCanvasElement canvasElement = createDomCanvasElement(); + int _pixelWidth = 0; + int _pixelHeight = 0; - DomCanvasRenderingContextBitmapRenderer? renderContext; + late final DomCanvasRenderingContextBitmapRenderer renderContext = + canvasElement.contextBitmapRenderer; - ui.Size? _currentCanvasPhysicalSize; - ui.Size? _currentRenderSize; double _currentDevicePixelRatio = -1; - bool _addedToScene = false; - void addToScene() { - if (!_addedToScene) { - CanvasKitRenderer.instance.sceneHost!.prepend(htmlElement); - } - _addedToScene = true; - } - /// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device /// pixels. /// @@ -71,92 +69,44 @@ class RenderCanvas { void _updateLogicalHtmlCanvasSize() { final double logicalWidth = _pixelWidth / window.devicePixelRatio; final double logicalHeight = _pixelHeight / window.devicePixelRatio; - final DomCSSStyleDeclaration style = canvasElement!.style; + final DomCSSStyleDeclaration style = canvasElement.style; style.width = '${logicalWidth}px'; style.height = '${logicalHeight}px'; + _currentDevicePixelRatio = window.devicePixelRatio; } - /// This function is expensive. + /// Render the given [bitmap] with this [RenderCanvas]. /// - /// 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 (canvasElement != null) { - canvasElement!.remove(); - } - - // If `physicalSize` is not precise, use a slightly bigger canvas. This way - // we ensure that the rendred picture covers the entire browser window. - _pixelWidth = physicalSize.width.ceil(); - _pixelHeight = physicalSize.height.ceil(); - final DomCanvasElement htmlCanvas = createDomCanvasElement( - width: _pixelWidth, - height: _pixelHeight, - ); - canvasElement = htmlCanvas; - renderContext = htmlCanvas.contextBitmapRenderer; - - // 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(); - htmlElement.append(htmlCanvas); + /// 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) { - if (size.isEmpty) { - throw CanvasKitError('Cannot create canvases of empty 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. - final ui.Size? previousRenderSize = _currentRenderSize; - if (previousRenderSize != null && - size.width == previousRenderSize.width && - size.height == previousRenderSize.height) { - // The existing canvas doesn't need to be resized. + 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; } - final ui.Size? previousCanvasSize = _currentCanvasPhysicalSize; // 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. - if (previousCanvasSize != null) { - canvasElement!.width = size.width.ceilToDouble(); - canvasElement!.height = size.height.ceilToDouble(); - _currentCanvasPhysicalSize = size; - _pixelWidth = size.width.ceil(); - _pixelHeight = size.height.ceil(); - _updateLogicalHtmlCanvasSize(); - } - - // This is the first frame we have rendered with this canvas. - if (_currentCanvasPhysicalSize == null) { - _addedToScene = false; - - _createNewCanvas(size); - _currentCanvasPhysicalSize = size; - } else if (window.devicePixelRatio != _currentDevicePixelRatio) { - _updateLogicalHtmlCanvasSize(); - } - - _currentDevicePixelRatio = window.devicePixelRatio; - _currentRenderSize = size; + _pixelWidth = size.width.ceil(); + _pixelHeight = size.height.ceil(); + canvasElement.width = _pixelWidth.toDouble(); + canvasElement.height = _pixelHeight.toDouble(); + _updateLogicalHtmlCanvasSize(); } void dispose() { diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 95de110f7f11c..a36054d489548 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -110,8 +110,6 @@ class Surface { Future rasterizeToCanvas( ui.Size frameSize, RenderCanvas canvas, List pictures) async { - canvas.ensureSize(frameSize); - final CkCanvas skCanvas = _surface!.getCanvas(); skCanvas.clear(const ui.Color(0x00000000)); pictures.forEach(skCanvas.drawPicture); @@ -135,7 +133,7 @@ class Surface { frameSize.height.toInt(), ))!; } - canvas.renderContext!.transferFromImageBitmap(bitmap); + canvas.render(bitmap); } /// Acquire a frame of the given [size] containing a drawable canvas. diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart index bd15218141df3..8ff3441fb99f5 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -72,16 +72,16 @@ void testMain() { expect(RenderCanvasFactory.debugUninitializedInstance, isNotNull); // Cause the surface and its canvas to be attached to the page - originalFactory.baseCanvas.ensureSize(const ui.Size(10, 10)); - originalFactory.baseCanvas.addToScene(); + 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++) { - overlays.add(originalFactory.getCanvas() - ..ensureSize(const ui.Size(10, 10)) - ..addToScene()); + final RenderCanvas canvas = originalFactory.getCanvas(); + CanvasKitRenderer.instance.sceneHost!.prepend(canvas.htmlElement); + overlays.add(canvas); } expect(originalFactory.debugSurfaceCount, 4); diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index 35d3a32056a7c..20a681be4993b 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -23,7 +23,7 @@ void testMain() { // Regression test for https://github.com/flutter/flutter/issues/75286 test('updates canvas logical size when device-pixel ratio changes', () { final RenderCanvas canvas = RenderCanvas(); - canvas.ensureSize(const ui.Size(10, 16)); + canvas._ensureSize(const ui.Size(10, 16)); expect(canvas.canvasElement!.width, 10); expect(canvas.canvasElement!.height, 16); @@ -33,7 +33,7 @@ void testMain() { // 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.ensureSize(const ui.Size(10, 16)); + canvas._ensureSize(const ui.Size(10, 16)); expect(canvas.canvasElement!.width, 10); expect(canvas.canvasElement!.height, 16); expect(canvas.canvasElement!.style.width, '5px'); @@ -42,7 +42,7 @@ void testMain() { // 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.ensureSize(const ui.Size(10, 16)); + canvas._ensureSize(const ui.Size(10, 16)); expect(canvas.canvasElement!.width, 10); expect(canvas.canvasElement!.height, 16); expect(canvas.canvasElement!.style.width, '20px'); @@ -50,7 +50,7 @@ void testMain() { // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 window.debugOverrideDevicePixelRatio(2.0); - canvas.ensureSize(const ui.Size(9.9, 15.9)); + canvas._ensureSize(const ui.Size(9.9, 15.9)); expect(canvas.canvasElement!.width, 10); expect(canvas.canvasElement!.height, 16); expect(canvas.canvasElement!.style.width, '5px'); From cde87399b302054ccbd2c3d919ce4aeda7dfa88c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 6 Sep 2023 15:26:57 -0700 Subject: [PATCH 25/30] Update tests --- lib/web_ui/lib/src/engine/dom.dart | 27 ++++++---- .../canvaskit/render_canvas_factory_test.dart | 5 +- .../test/canvaskit/render_canvas_test.dart | 53 ++++++++++--------- lib/web_ui/test/ui/image_golden_test.dart | 2 +- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 26e5558f75e5d..737f72454022e 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -199,9 +199,9 @@ external DomIntl get domIntl; external DomSymbol get domSymbol; @JS('createImageBitmap') -external JSPromise _createImageBitmap(DomCanvasElement canvas); -Future createImageBitmap(DomCanvasElement canvas) => - js_util.promiseToFuture(_createImageBitmap(canvas)); +external JSPromise _createImageBitmap(JSAny source); +Future createImageBitmap(JSAny source) => + js_util.promiseToFuture(_createImageBitmap(source)); @JS('createImageBitmap') external JSPromise _createSizedImageBitmap(DomCanvasElement canvas, JSNumber sx, @@ -211,6 +211,15 @@ Future createSizedImageBitmap( 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); @@ -1433,10 +1442,13 @@ extension DomCanvasRenderingContextBitmapRendererExtension @staticInterop class DomImageData { external factory DomImageData._(JSAny? data, JSNumber sw, JSNumber sh); + external factory DomImageData._empty(JSNumber sw, JSNumber sh); } -DomImageData createDomImageData(Object? data, int sw, int sh) => - DomImageData._(data?.toJSAnyShallow, sw.toJS, sh.toJS); +DomImageData createDomImageData(Object data, int sw, int sh) => + DomImageData._(data.toJSAnyShallow, sw.toJS, sh.toJS); +DomImageData createBlankDomImageData(int sw, int sh) => + DomImageData._empty(sw.toJS, sh.toJS); extension DomImageDataExtension on DomImageData { @JS('data') @@ -1454,7 +1466,6 @@ extension DomImageBitmapExtension on DomImageBitmap { external void close(); } - @JS() @staticInterop class DomCanvasPattern {} @@ -2301,9 +2312,7 @@ DomBlob createDomBlob(List parts, [Map? options]) { return DomBlob(parts.toJSAnyShallow as JSArray); } else { return DomBlob.withOptions( - parts.toJSAnyShallow as JSArray, - options.toJSAnyDeep - ); + parts.toJSAnyShallow as JSArray, options.toJSAnyDeep); } } diff --git a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart index 8ff3441fb99f5..70aa4e6073ffa 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_factory_test.dart @@ -5,7 +5,6 @@ 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'; @@ -65,7 +64,7 @@ void testMain() { test('hot restart', () { void expectDisposed(RenderCanvas canvas) { - expect(canvas.canvasElement!.isConnected, isFalse); + expect(canvas.canvasElement.isConnected, isFalse); } final RenderCanvasFactory originalFactory = RenderCanvasFactory.instance; @@ -74,7 +73,7 @@ void testMain() { // 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); + expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue); // Create a few overlay canvases final List overlays = []; diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index 20a681be4993b..4fa7cc5e52687 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -16,45 +16,48 @@ void main() { void testMain() { group('CanvasKit', () { setUpCanvasKitTest(); - setUp(() { + 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', () { + test('updates canvas logical size when device-pixel ratio changes', + () async { final RenderCanvas canvas = RenderCanvas(); - canvas._ensureSize(const ui.Size(10, 16)); + 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'); + 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._ensureSize(const ui.Size(10, 16)); - expect(canvas.canvasElement!.width, 10); - expect(canvas.canvasElement!.height, 16); - expect(canvas.canvasElement!.style.width, '5px'); - expect(canvas.canvasElement!.style.height, '8px'); + 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._ensureSize(const ui.Size(10, 16)); - expect(canvas.canvasElement!.width, 10); - expect(canvas.canvasElement!.height, 16); - expect(canvas.canvasElement!.style.width, '20px'); - expect(canvas.canvasElement!.style.height, '32px'); - - // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 - window.debugOverrideDevicePixelRatio(2.0); - canvas._ensureSize(const ui.Size(9.9, 15.9)); - expect(canvas.canvasElement!.width, 10); - expect(canvas.canvasElement!.height, 16); - expect(canvas.canvasElement!.style.width, '5px'); - expect(canvas.canvasElement!.style.height, '8px'); + 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/ui/image_golden_test.dart b/lib/web_ui/test/ui/image_golden_test.dart index fccb86c8b2951..9894ccac310a9 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).toDart)! as DomImageBitmap; + final DomImageBitmap bitmap = (await createImageBitmap(image as JSAny))!; return renderer.createImageFromImageBitmap(bitmap); }); } From 9fea067c566bfeaff6410d6b7093fda92c2cd482 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 6 Sep 2023 15:28:03 -0700 Subject: [PATCH 26/30] Fix warning --- lib/web_ui/test/canvaskit/render_canvas_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/render_canvas_test.dart b/lib/web_ui/test/canvaskit/render_canvas_test.dart index 4fa7cc5e52687..75a0cfba2897c 100644 --- a/lib/web_ui/test/canvaskit/render_canvas_test.dart +++ b/lib/web_ui/test/canvaskit/render_canvas_test.dart @@ -5,7 +5,6 @@ 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'; From 3a90473fde75e41e362ac6607e9b332fb30662a3 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 6 Sep 2023 15:44:55 -0700 Subject: [PATCH 27/30] Fix analysis warnings --- .../src/engine/canvaskit/render_canvas.dart | 2 -- lib/web_ui/test/engine/scene_view_test.dart | 36 ++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) 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 2021985ff9505..7793cfbe6cbce 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/render_canvas.dart @@ -8,8 +8,6 @@ import 'package:ui/ui.dart' as ui; import '../dom.dart'; import '../window.dart'; -import 'renderer.dart'; -import 'util.dart'; /// A visible (on-screen) canvas that can display bitmaps produced by CanvasKit /// in the (off-screen) SkSurface which is backed by an OffscreenCanvas. diff --git a/lib/web_ui/test/engine/scene_view_test.dart b/lib/web_ui/test/engine/scene_view_test.dart index 0b475d8842824..d054059c4b443 100644 --- a/lib/web_ui/test/engine/scene_view_test.dart +++ b/lib/web_ui/test/engine/scene_view_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:js_interop'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -19,17 +18,18 @@ 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 createImageBitmap( - scratchCanvasElement as JSAny, - (x: 0, y: 0, width: cullRect.width.toInt(), height: cullRect.height.toInt()) - ).toDart)! as DomImageBitmap; + final DomImageBitmap bitmap = (await createSizedImageBitmap( + scratchCanvasElement, + 0, + 0, + cullRect.width.toInt(), + cullRect.height.toInt()))!; return bitmap; } } @@ -58,9 +58,11 @@ void testMain() { final List children = sceneElement.children.toList(); expect(children.length, 1); final DomElement containerElement = children.first; - expect(containerElement.tagName, equalsIgnoringCase('flt-canvas-container')); + expect( + containerElement.tagName, equalsIgnoringCase('flt-canvas-container')); - final List containerChildren = containerElement.children.toList(); + final List containerChildren = + containerElement.children.toList(); expect(containerChildren.length, 1); final DomElement canvasElement = containerChildren.first; final DomCSSStyleDeclaration style = canvasElement.style; @@ -76,12 +78,11 @@ void testMain() { debugOverrideDevicePixelRatio(2.0); final PlatformView platformView = PlatformView( - 1, - const ui.Size(100, 120), - const PlatformViewStyling( - position: PlatformViewPosition.offset(ui.Offset(50, 80)), - ) - ); + 1, + const ui.Size(100, 120), + const PlatformViewStyling( + position: PlatformViewPosition.offset(ui.Offset(50, 80)), + )); final EngineRootLayer rootLayer = EngineRootLayer(); rootLayer.slices.add(PlatformViewSlice([platformView], null)); final EngineScene scene = EngineScene(rootLayer); @@ -91,7 +92,8 @@ void testMain() { final List children = sceneElement.children.toList(); expect(children.length, 1); final DomElement containerElement = children.first; - expect(containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot')); + expect( + containerElement.tagName, equalsIgnoringCase('flt-platform-view-slot')); final DomCSSStyleDeclaration style = containerElement.style; expect(style.left, '25px'); From 17d7412fc4dc16a4e83b450aaf3d51d00dd70397 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 12 Sep 2023 11:06:58 -0700 Subject: [PATCH 28/30] Delete empty branch --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index a36054d489548..05cb04fa83ed1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -229,7 +229,7 @@ class Surface { _createNewCanvas(size); _currentCanvasPhysicalSize = size; - } else if (window.devicePixelRatio != _currentDevicePixelRatio) {} + } _currentDevicePixelRatio = window.devicePixelRatio; _currentSurfaceSize = size; From 9a11fae3eda34e2a46e35e73910f529906637b2a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 12 Sep 2023 12:22:42 -0700 Subject: [PATCH 29/30] Remove unused variable --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 05cb04fa83ed1..c75355fdab2d9 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -153,7 +153,6 @@ class Surface { ui.Size? _currentCanvasPhysicalSize; ui.Size? _currentSurfaceSize; - double _currentDevicePixelRatio = -1; /// This is only valid after the first frame or if [ensureSurface] has been /// called @@ -231,7 +230,6 @@ class Surface { _currentCanvasPhysicalSize = size; } - _currentDevicePixelRatio = window.devicePixelRatio; _currentSurfaceSize = size; _surface?.dispose(); _surface = _createNewSurface(size); From 32f1c24a0850047d9a33fd4384091c70958db7c3 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 12 Sep 2023 12:31:24 -0700 Subject: [PATCH 30/30] Remove unused import --- lib/web_ui/lib/src/engine/canvaskit/surface.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index c75355fdab2d9..db2277c65ac6b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -11,7 +11,6 @@ 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';