[CanvasKit] Only render one frame at a time. (flutter/engine#50055)
Only have one additional queued frame. If more than one frame is requested while the current frame is rendering, only the last frame is queued. Fixes https://github.com/flutter/flutter/issues/140981 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
@@ -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<void> 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;
|
||||
}
|
||||
|
||||
@@ -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<void> 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<void> completer = Completer<void>();
|
||||
renderQueue.next = (scene: scene, completer: completer);
|
||||
return completer.future;
|
||||
}
|
||||
final Completer<void> completer = Completer<void>();
|
||||
renderQueue.current = (scene: scene, completer: completer);
|
||||
unawaited(_kickRenderLoop(rasterizer));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
176
engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart
Normal file
176
engine/src/flutter/lib/web_ui/test/canvaskit/renderer_test.dart
Normal file
@@ -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<EngineFlutterView, TestViewRasterizer> viewRasterizers =
|
||||
<EngineFlutterView, TestViewRasterizer>{};
|
||||
|
||||
@override
|
||||
TestViewRasterizer createViewRasterizer(EngineFlutterView view) {
|
||||
return viewRasterizers.putIfAbsent(view, () => TestViewRasterizer(view));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
void setResourceCacheMaxBytes(int bytes) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
List<LayerTree> treesRenderedInView(EngineFlutterView view) {
|
||||
return viewRasterizers[view]!.treesRendered;
|
||||
}
|
||||
}
|
||||
|
||||
class TestViewRasterizer extends ViewRasterizer {
|
||||
TestViewRasterizer(super.view);
|
||||
|
||||
List<LayerTree> treesRendered = <LayerTree>[];
|
||||
|
||||
@override
|
||||
DisplayCanvasFactory<DisplayCanvas> get displayFactory =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void prepareToDraw() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> draw(LayerTree tree) async {
|
||||
treesRendered.add(tree);
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rasterizeToCanvas(
|
||||
DisplayCanvas canvas, List<CkPicture> pictures) {
|
||||
// No-op
|
||||
return Future<void>.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<LayerTree> treesToRender = <LayerTree>[];
|
||||
final List<Future<void>> renderFutures = <Future<void>>[];
|
||||
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<LayerTree> 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<EngineFlutterView, List<LayerTree>> treesToRender =
|
||||
<EngineFlutterView, List<LayerTree>>{};
|
||||
treesToRender[testView1] = <LayerTree>[];
|
||||
treesToRender[testView2] = <LayerTree>[];
|
||||
treesToRender[testView3] = <LayerTree>[];
|
||||
final List<Future<void>> renderFutures = <Future<void>>[];
|
||||
|
||||
for (int i = 1; i < 20; i++) {
|
||||
for (final EngineFlutterView testView in <EngineFlutterView>[
|
||||
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<LayerTree> treesRenderedInView1 =
|
||||
testRasterizer.treesRenderedInView(testView1);
|
||||
final List<LayerTree> treesToRenderInView1 = treesToRender[testView1]!;
|
||||
expect(treesRenderedInView1.length, 2);
|
||||
expect(treesRenderedInView1.first, treesToRenderInView1.first);
|
||||
expect(treesRenderedInView1.last, treesToRenderInView1.last);
|
||||
|
||||
final List<LayerTree> treesRenderedInView2 =
|
||||
testRasterizer.treesRenderedInView(testView2);
|
||||
final List<LayerTree> treesToRenderInView2 = treesToRender[testView2]!;
|
||||
expect(treesRenderedInView2.length, 2);
|
||||
expect(treesRenderedInView2.first, treesToRenderInView2.first);
|
||||
expect(treesRenderedInView2.last, treesToRenderInView2.last);
|
||||
|
||||
final List<LayerTree> treesRenderedInView3 =
|
||||
testRasterizer.treesRenderedInView(testView3);
|
||||
final List<LayerTree> treesToRenderInView3 = treesToRender[testView3]!;
|
||||
expect(treesRenderedInView3.length, 2);
|
||||
expect(treesRenderedInView3.first, treesToRenderInView3.first);
|
||||
expect(treesRenderedInView3.last, treesToRenderInView3.last);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user