diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 650b5bea80..58a23e871c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -1,6 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; @@ -22,6 +23,9 @@ abstract class ViewRasterizer { /// The view this rasterizer renders into. final EngineFlutterView view; + /// The queue of render requests for this view. + final RenderQueue queue = RenderQueue(); + /// The size of the current frame being rasterized. ui.Size currentFrameSize = ui.Size.zero; @@ -121,3 +125,21 @@ abstract class DisplayCanvas { /// Disposes this overlay. void dispose(); } + +/// Encapsulates a request to render a [ui.Scene]. Contains the scene to render +/// and a [Completer] which completes when the scene has been rendered. +typedef RenderRequest = ({ + ui.Scene scene, + Completer completer, +}); + +/// A per-view queue of render requests. Only contains the current render +/// request and the next render request. If a new render request is made before +/// the current request is complete, then the next render request is replaced +/// with the most recently requested render and the other one is dropped. +class RenderQueue { + RenderQueue(); + + RenderRequest? current; + RenderRequest? next; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 51b3093e0f..f4fdaef67c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -44,7 +44,7 @@ class CanvasKitRenderer implements Renderer { DomElement? _sceneHost; DomElement? get sceneHost => _sceneHost; - final Rasterizer _rasterizer = _createRasterizer(); + Rasterizer _rasterizer = _createRasterizer(); static Rasterizer _createRasterizer() { if (isSafari || isFirefox) { @@ -53,6 +53,11 @@ class CanvasKitRenderer implements Renderer { return OffscreenCanvasRasterizer(); } + /// Override the rasterizer with the given [_rasterizer]. Used in tests. + void debugOverrideRasterizer(Rasterizer testRasterizer) { + _rasterizer = testRasterizer; + } + set resourceCacheMaxBytes(int bytes) => _rasterizer.setResourceCacheMaxBytes(bytes); @@ -404,8 +409,47 @@ class CanvasKitRenderer implements Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style) => CkParagraphBuilder(style); + // TODO(harryterkelsen): Merge this logic with the async logic in + // [EngineScene], https://github.com/flutter/flutter/issues/142072. @override Future renderScene(ui.Scene scene, ui.FlutterView view) async { + assert(_rasterizers.containsKey(view.viewId), + "Unable to render to a view which hasn't been registered"); + final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; + final RenderQueue renderQueue = rasterizer.queue; + if (renderQueue.current != null) { + // If a scene is already queued up, drop it and queue this one up instead + // so that the scene view always displays the most recently requested scene. + renderQueue.next?.completer.complete(); + final Completer completer = Completer(); + renderQueue.next = (scene: scene, completer: completer); + return completer.future; + } + final Completer completer = Completer(); + renderQueue.current = (scene: scene, completer: completer); + unawaited(_kickRenderLoop(rasterizer)); + return completer.future; + } + + Future _kickRenderLoop(ViewRasterizer rasterizer) async { + final RenderQueue renderQueue = rasterizer.queue; + final RenderRequest current = renderQueue.current!; + try { + await _renderScene(current.scene, rasterizer); + current.completer.complete(); + } catch (error, stackTrace) { + current.completer.completeError(error, stackTrace); + } + renderQueue.current = renderQueue.next; + renderQueue.next = null; + if (renderQueue.current == null) { + return; + } else { + return _kickRenderLoop(rasterizer); + } + } + + Future _renderScene(ui.Scene scene, ViewRasterizer rasterizer) async { // "Build finish" and "raster start" happen back-to-back because we // render on the same thread, so there's no overhead from hopping to // another thread. @@ -416,10 +460,6 @@ class CanvasKitRenderer implements Renderer { frameTimingsOnBuildFinish(); frameTimingsOnRasterStart(); - assert(_rasterizers.containsKey(view.viewId), - "Unable to render to a view which hasn't been registered"); - final ViewRasterizer rasterizer = _rasterizers[view.viewId]!; - await rasterizer.draw((scene as LayerScene).layerTree); frameTimingsOnRasterFinish(); } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/multi_view_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/multi_view_test.dart index 62b499226e..84dca65068 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/multi_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/multi_view_test.dart @@ -71,7 +71,8 @@ void testMain() { }); // Issue https://github.com/flutter/flutter/issues/142094 - test('does not reset platform view factories when disposing a view', () async { + test('does not reset platform view factories when disposing a view', + () async { expect(PlatformViewManager.instance.knowsViewType('self-test'), isFalse); final EngineFlutterView view = EngineFlutterView( @@ -89,8 +90,14 @@ void testMain() { isNull, ); - expect(PlatformViewManager.instance.knowsViewType(ui_web.PlatformViewRegistry.defaultVisibleViewType), isTrue); - expect(PlatformViewManager.instance.knowsViewType(ui_web.PlatformViewRegistry.defaultInvisibleViewType), isTrue); + expect( + PlatformViewManager.instance.knowsViewType( + ui_web.PlatformViewRegistry.defaultVisibleViewType), + isTrue); + expect( + PlatformViewManager.instance.knowsViewType( + ui_web.PlatformViewRegistry.defaultInvisibleViewType), + isTrue); }); }); } diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart new file mode 100644 index 0000000000..6dc6264624 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart @@ -0,0 +1,176 @@ +// 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'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +class TestRasterizer extends Rasterizer { + Map viewRasterizers = + {}; + + @override + TestViewRasterizer createViewRasterizer(EngineFlutterView view) { + return viewRasterizers.putIfAbsent(view, () => TestViewRasterizer(view)); + } + + @override + void dispose() { + // Do nothing + } + + @override + void setResourceCacheMaxBytes(int bytes) { + // Do nothing + } + + List treesRenderedInView(EngineFlutterView view) { + return viewRasterizers[view]!.treesRendered; + } +} + +class TestViewRasterizer extends ViewRasterizer { + TestViewRasterizer(super.view); + + List treesRendered = []; + + @override + DisplayCanvasFactory get displayFactory => + throw UnimplementedError(); + + @override + void prepareToDraw() { + // Do nothing + } + + @override + Future draw(LayerTree tree) async { + treesRendered.add(tree); + return Future.value(); + } + + @override + Future rasterizeToCanvas( + DisplayCanvas canvas, List pictures) { + // No-op + return Future.value(); + } +} + +void testMain() { + group('Renderer', () { + setUpCanvasKitTest(); + + test('always renders most recent picture and skips intermediate pictures', + () async { + final TestRasterizer testRasterizer = TestRasterizer(); + CanvasKitRenderer.instance.debugOverrideRasterizer(testRasterizer); + + // Create another view to render into to force the renderer to make + // a [ViewRasterizer] for it. + final EngineFlutterView testView = EngineFlutterView( + EnginePlatformDispatcher.instance, createDomElement('test-view')); + EnginePlatformDispatcher.instance.viewManager.registerView(testView); + + final List treesToRender = []; + final List> renderFutures = >[]; + for (int i = 1; i < 20; i++) { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawRect(const ui.Rect.fromLTWH(0, 0, 50, 50), + ui.Paint()..color = const ui.Color(0xff00ff00)); + final ui.Picture picture = recorder.endRecording(); + final ui.SceneBuilder builder = ui.SceneBuilder(); + builder.addPicture(ui.Offset.zero, picture); + final ui.Scene scene = builder.build(); + treesToRender.add((scene as LayerScene).layerTree); + renderFutures + .add(CanvasKitRenderer.instance.renderScene(scene, testView)); + } + await Future.wait(renderFutures); + + // Should just render the first and last pictures and skip the one inbetween. + final List treesRendered = + testRasterizer.treesRenderedInView(testView); + expect(treesRendered.length, 2); + expect(treesRendered.first, treesToRender.first); + expect(treesRendered.last, treesToRender.last); + }); + + test('can render multiple frames at once into multiple views', () async { + final TestRasterizer testRasterizer = TestRasterizer(); + CanvasKitRenderer.instance.debugOverrideRasterizer(testRasterizer); + + // Create another view to render into to force the renderer to make + // a [ViewRasterizer] for it. + final EngineFlutterView testView1 = EngineFlutterView( + EnginePlatformDispatcher.instance, createDomElement('test-view')); + EnginePlatformDispatcher.instance.viewManager.registerView(testView1); + final EngineFlutterView testView2 = EngineFlutterView( + EnginePlatformDispatcher.instance, createDomElement('test-view')); + EnginePlatformDispatcher.instance.viewManager.registerView(testView2); + final EngineFlutterView testView3 = EngineFlutterView( + EnginePlatformDispatcher.instance, createDomElement('test-view')); + EnginePlatformDispatcher.instance.viewManager.registerView(testView3); + + final Map> treesToRender = + >{}; + treesToRender[testView1] = []; + treesToRender[testView2] = []; + treesToRender[testView3] = []; + final List> renderFutures = >[]; + + for (int i = 1; i < 20; i++) { + for (final EngineFlutterView testView in [ + testView1, + testView2, + testView3, + ]) { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawRect(const ui.Rect.fromLTWH(0, 0, 50, 50), + ui.Paint()..color = const ui.Color(0xff00ff00)); + final ui.Picture picture = recorder.endRecording(); + final ui.SceneBuilder builder = ui.SceneBuilder(); + builder.addPicture(ui.Offset.zero, picture); + final ui.Scene scene = builder.build(); + treesToRender[testView]!.add((scene as LayerScene).layerTree); + renderFutures + .add(CanvasKitRenderer.instance.renderScene(scene, testView)); + } + } + await Future.wait(renderFutures); + + // Should just render the first and last pictures and skip the one inbetween. + final List treesRenderedInView1 = + testRasterizer.treesRenderedInView(testView1); + final List treesToRenderInView1 = treesToRender[testView1]!; + expect(treesRenderedInView1.length, 2); + expect(treesRenderedInView1.first, treesToRenderInView1.first); + expect(treesRenderedInView1.last, treesToRenderInView1.last); + + final List treesRenderedInView2 = + testRasterizer.treesRenderedInView(testView2); + final List treesToRenderInView2 = treesToRender[testView2]!; + expect(treesRenderedInView2.length, 2); + expect(treesRenderedInView2.first, treesToRenderInView2.first); + expect(treesRenderedInView2.last, treesToRenderInView2.last); + + final List treesRenderedInView3 = + testRasterizer.treesRenderedInView(testView3); + final List treesToRenderInView3 = treesToRender[testView3]!; + expect(treesRenderedInView3.length, 2); + expect(treesRenderedInView3.first, treesToRenderInView3.first); + expect(treesRenderedInView3.last, treesToRenderInView3.last); + }); + }); +}