[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:
Harry Terkelsen
2024-01-31 14:00:08 -08:00
committed by GitHub
parent c4beeacb6f
commit 2db9940615
4 changed files with 253 additions and 8 deletions

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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);
});
});
}

View 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);
});
});
}