Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit c99c6bd

Browse files
Reland "[web] Enforce onDrawFrame/onBeginFrame render rule" (#49336)
Enforces the render rule on the web except in the HTML renderer, which must still render to the DOM even outside of `onDrawFrame` or `onBegineFrame` scopes in order for golden tests to continue to work in the framework. This is a reland of #49214 with one change (checking for `renderer.rendererTag == 'html'` in the `render` method). ## 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
1 parent a2f296c commit c99c6bd

29 files changed

+218
-165
lines changed

lib/web_ui/lib/platform_dispatcher.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ abstract class PlatformDispatcher {
8080

8181
void scheduleFrame();
8282

83-
void render(Scene scene, [FlutterView view]);
83+
Future<void> render(Scene scene, [FlutterView view]);
8484

8585
AccessibilityFeatures get accessibilityFeatures;
8686

lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ class HtmlViewEmbedder {
364364
sceneHost.append(_svgPathDefs!);
365365
}
366366

367-
void submitFrame() {
367+
Future<void> submitFrame() async {
368368
final ViewListDiffResult? diffResult =
369369
(_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty)
370370
? null
@@ -388,7 +388,7 @@ class HtmlViewEmbedder {
388388
_context.pictureRecorders[pictureRecorderIndex].endRecording());
389389
pictureRecorderIndex++;
390390
}
391-
rasterizer.rasterizeToCanvas(overlay, pictures);
391+
await rasterizer.rasterizeToCanvas(overlay, pictures);
392392
}
393393
for (final CkPictureRecorder recorder
394394
in _context.pictureRecordersCreatedDuringPreroll) {

lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Rasterizer {
3030

3131
/// Creates a new frame from this rasterizer's surface, draws the given
3232
/// [LayerTree] into it, and then submits the frame.
33-
void draw(LayerTree layerTree) {
33+
Future<void> draw(LayerTree layerTree) async {
3434
final ui.Size frameSize = view.physicalSize;
3535
if (frameSize.isEmpty) {
3636
// Available drawing area is empty. Skip drawing.
@@ -49,10 +49,10 @@ class Rasterizer {
4949
compositorFrame.raster(layerTree, ignoreRasterCache: true);
5050

5151
sceneHost.prepend(renderCanvasFactory.baseCanvas.htmlElement);
52-
rasterizeToCanvas(renderCanvasFactory.baseCanvas,
52+
await rasterizeToCanvas(renderCanvasFactory.baseCanvas,
5353
<CkPicture>[pictureRecorder.endRecording()]);
5454

55-
viewEmbedder.submitFrame();
55+
await viewEmbedder.submitFrame();
5656
}
5757

5858
/// Disposes of this rasterizer.

lib/web_ui/lib/src/engine/canvaskit/renderer.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ class CanvasKitRenderer implements Renderer {
402402
CkParagraphBuilder(style);
403403

404404
@override
405-
void renderScene(ui.Scene scene, ui.FlutterView view) {
405+
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) async {
406406
// "Build finish" and "raster start" happen back-to-back because we
407407
// render on the same thread, so there's no overhead from hopping to
408408
// another thread.
@@ -417,7 +417,7 @@ class CanvasKitRenderer implements Renderer {
417417
"Unable to render to a view which hasn't been registered");
418418
final Rasterizer rasterizer = _rasterizers[view.viewId]!;
419419

420-
rasterizer.draw((scene as LayerScene).layerTree);
420+
await rasterizer.draw((scene as LayerScene).layerTree);
421421
frameTimingsOnRasterFinish();
422422
}
423423

lib/web_ui/lib/src/engine/canvaskit/surface.dart

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,22 +115,18 @@ class Surface {
115115
_surface!.flush();
116116

117117
if (browserSupportsCreateImageBitmap) {
118-
DomImageBitmap bitmap;
118+
JSObject bitmapSource;
119119
if (Surface.offscreenCanvasSupported) {
120-
bitmap = (await createImageBitmap(_offscreenCanvas! as JSObject, (
121-
x: 0,
122-
y: _pixelHeight - frameSize.height.toInt(),
123-
width: frameSize.width.toInt(),
124-
height: frameSize.height.toInt(),
125-
)).toDart)! as DomImageBitmap;
120+
bitmapSource = _offscreenCanvas! as JSObject;
126121
} else {
127-
bitmap = (await createImageBitmap(_canvasElement! as JSObject, (
128-
x: 0,
129-
y: _pixelHeight - frameSize.height.toInt(),
130-
width: frameSize.width.toInt(),
131-
height: frameSize.height.toInt()
132-
)).toDart)! as DomImageBitmap;
122+
bitmapSource = _canvasElement! as JSObject;
133123
}
124+
final DomImageBitmap bitmap = await createImageBitmap(bitmapSource, (
125+
x: 0,
126+
y: _pixelHeight - frameSize.height.toInt(),
127+
width: frameSize.width.toInt(),
128+
height: frameSize.height.toInt(),
129+
));
134130
canvas.render(bitmap);
135131
} else {
136132
// If the browser doesn't support `createImageBitmap` (e.g. Safari 14)

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,16 @@ external JSPromise<JSAny?> _createImageBitmap2(
210210
JSNumber width,
211211
JSNumber height,
212212
);
213-
JSPromise<JSAny?> createImageBitmap(JSAny source,
213+
Future<DomImageBitmap> createImageBitmap(JSAny source,
214214
[({int x, int y, int width, int height})? bounds]) {
215+
JSPromise<JSAny?> jsPromise;
215216
if (bounds != null) {
216-
return _createImageBitmap2(source, bounds.x.toJS, bounds.y.toJS,
217+
jsPromise = _createImageBitmap2(source, bounds.x.toJS, bounds.y.toJS,
217218
bounds.width.toJS, bounds.height.toJS);
218219
} else {
219-
return _createImageBitmap1(source);
220+
jsPromise = _createImageBitmap1(source);
220221
}
222+
return js_util.promiseToFuture<DomImageBitmap>(jsPromise);
221223
}
222224

223225
@JS()

lib/web_ui/lib/src/engine/html/renderer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ class HtmlRenderer implements Renderer {
323323
CanvasParagraphBuilder(style as EngineParagraphStyle);
324324

325325
@override
326-
void renderScene(ui.Scene scene, ui.FlutterView view) {
326+
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) async {
327327
final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!;
328328
implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!);
329329
frameTimingsOnRasterFinish();

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
213213
}
214214
}
215215

216+
/// A set of views which have rendered in the current `onBeginFrame` or
217+
/// `onDrawFrame` scope.
218+
Set<ui.FlutterView>? _viewsRenderedInCurrentFrame;
219+
216220
/// A callback invoked when any window begins a frame.
217221
///
218222
/// A callback that is invoked to notify the application that it is an
@@ -235,7 +239,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
235239
/// Engine code should use this method instead of the callback directly.
236240
/// Otherwise zones won't work properly.
237241
void invokeOnBeginFrame(Duration duration) {
242+
_viewsRenderedInCurrentFrame = <ui.FlutterView>{};
238243
invoke1<Duration>(_onBeginFrame, _onBeginFrameZone, duration);
244+
_viewsRenderedInCurrentFrame = null;
239245
}
240246

241247
/// A callback that is invoked for each frame after [onBeginFrame] has
@@ -256,7 +262,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
256262
/// Engine code should use this method instead of the callback directly.
257263
/// Otherwise zones won't work properly.
258264
void invokeOnDrawFrame() {
265+
_viewsRenderedInCurrentFrame = <ui.FlutterView>{};
259266
invoke(_onDrawFrame, _onDrawFrameZone);
267+
_viewsRenderedInCurrentFrame = null;
260268
}
261269

262270
/// A callback that is invoked when pointer data is available.
@@ -753,14 +761,27 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
753761
/// * [RendererBinding], the Flutter framework class which manages layout and
754762
/// painting.
755763
@override
756-
void render(ui.Scene scene, [ui.FlutterView? view]) {
764+
Future<void> render(ui.Scene scene, [ui.FlutterView? view]) async {
757765
assert(view != null || implicitView != null,
758766
'Calling render without a FlutterView');
759767
if (view == null && implicitView == null) {
760768
// If there is no view to render into, then this is a no-op.
761769
return;
762770
}
763-
renderer.renderScene(scene, view ?? implicitView!);
771+
final ui.FlutterView viewToRender = view ?? implicitView!;
772+
773+
// Only render in an `onDrawFrame` or `onBeginFrame` scope. This is checked
774+
// by checking if the `_viewsRenderedInCurrentFrame` is non-null and this
775+
// view hasn't been rendered already in this scope.
776+
final bool shouldRender =
777+
_viewsRenderedInCurrentFrame?.add(viewToRender) ?? false;
778+
// TODO(harryterkelsen): HTML renderer needs to violate the render rule in
779+
// order to perform golden tests in Flutter framework because on the HTML
780+
// renderer, golden tests render to DOM and then take a browser screenshot,
781+
// https://github.com/flutter/flutter/issues/137073.
782+
if (shouldRender || renderer.rendererTag == 'html') {
783+
await renderer.renderScene(scene, viewToRender);
784+
}
764785
}
765786

766787
/// Additional accessibility features that may be enabled by the platform.
@@ -1275,7 +1296,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
12751296
String get defaultRouteName {
12761297
// TODO(mdebbar): What should we do in multi-view mode?
12771298
// https://github.com/flutter/flutter/issues/139174
1278-
return _defaultRouteName ??= implicitView?.browserHistory.currentPath ?? '/';
1299+
return _defaultRouteName ??=
1300+
implicitView?.browserHistory.currentPath ?? '/';
12791301
}
12801302

12811303
/// Lazily initialized when the `defaultRouteName` getter is invoked.

lib/web_ui/lib/src/engine/renderer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,5 @@ abstract class Renderer {
222222

223223
ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style);
224224

225-
FutureOr<void> renderScene(ui.Scene scene, ui.FlutterView view);
225+
Future<void> renderScene(ui.Scene scene, ui.FlutterView view);
226226
}

lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class SkwasmRenderer implements Renderer {
150150
}
151151

152152
@override
153-
void renderScene(ui.Scene scene, ui.FlutterView view) {
153+
Future<void> renderScene(ui.Scene scene, ui.FlutterView view) {
154154
throw UnimplementedError('Skwasm not implemented on this platform.');
155155
}
156156

lib/web_ui/test/canvaskit/backdrop_filter_golden_test.dart

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import 'package:test/test.dart';
77
import 'package:ui/src/engine.dart';
88
import 'package:ui/ui.dart' as ui;
99

10-
import 'package:web_engine_tester/golden_tester.dart';
11-
1210
import 'common.dart';
1311

1412
void main() {
@@ -49,8 +47,8 @@ void testMain() {
4947
builder.pushOffset(0, 0);
5048
builder.addPicture(ui.Offset.zero, checkerboard);
5149
builder.pushBackdropFilter(ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10));
52-
CanvasKitRenderer.instance.renderScene(builder.build(), implicitView);
53-
await matchGoldenFile('canvaskit_backdropfilter_blur_edges.png',
50+
await matchSceneGolden(
51+
'canvaskit_backdropfilter_blur_edges.png', builder.build(),
5452
region: region);
5553
});
5654
test('ImageFilter with ColorFilter as child', () async {
@@ -62,9 +60,7 @@ void testMain() {
6260
final CkPictureRecorder recorder = CkPictureRecorder();
6361
final CkCanvas canvas = recorder.beginRecording(region);
6462
final ui.ColorFilter colorFilter = ui.ColorFilter.mode(
65-
const ui.Color(0XFF00FF00).withOpacity(0.55),
66-
ui.BlendMode.darken
67-
);
63+
const ui.Color(0XFF00FF00).withOpacity(0.55), ui.BlendMode.darken);
6864

6965
// using a colorFilter as an imageFilter for backDrop filter
7066
builder.pushBackdropFilter(colorFilter);
@@ -75,7 +71,10 @@ void testMain() {
7571
);
7672
final CkPicture redCircle1 = recorder.endRecording();
7773
builder.addPicture(ui.Offset.zero, redCircle1);
78-
await matchSceneGolden('canvaskit_red_circle_green_backdrop_colorFilter.png', builder.build(), region: region);
74+
await matchSceneGolden(
75+
'canvaskit_red_circle_green_backdrop_colorFilter.png',
76+
builder.build(),
77+
region: region);
7978
});
8079
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520
8180
}, skip: isSafari || isFirefox);

lib/web_ui/test/canvaskit/canvas_golden_test.dart

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import 'package:ui/src/engine.dart';
1111
import 'package:ui/ui.dart' as ui;
1212
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
1313

14-
import 'package:web_engine_tester/golden_tester.dart';
15-
1614
import 'common.dart';
1715

1816
void main() {
@@ -147,16 +145,14 @@ void testMain() {
147145
builder.pushOffset(0, 0);
148146
builder.addPicture(ui.Offset.zero, picture);
149147
final LayerScene scene = builder.build();
150-
CanvasKitRenderer.instance.renderScene(scene, implicitView);
148+
await renderScene(scene);
151149

152150
// Now draw an empty layer tree and confirm that the red rectangle is
153151
// no longer drawn.
154152
final LayerSceneBuilder emptySceneBuilder = LayerSceneBuilder();
155153
emptySceneBuilder.pushOffset(0, 0);
156154
final LayerScene emptyScene = emptySceneBuilder.build();
157-
CanvasKitRenderer.instance.renderScene(emptyScene, implicitView);
158-
159-
await matchGoldenFile('canvaskit_empty_scene.png',
155+
await matchSceneGolden('canvaskit_empty_scene.png', emptyScene,
160156
region: const ui.Rect.fromLTRB(0, 0, 100, 100));
161157
});
162158

@@ -211,9 +207,8 @@ void testMain() {
211207
sb.pop();
212208

213209
// The below line should not throw an error.
214-
CanvasKitRenderer.instance.renderScene(sb.build(), implicitView);
215-
216-
await matchGoldenFile('cross_overlay_resources.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100));
210+
await matchSceneGolden('cross_overlay_resources.png', sb.build(),
211+
region: const ui.Rect.fromLTRB(0, 0, 100, 100));
217212
});
218213
});
219214
}

lib/web_ui/test/canvaskit/common.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import 'package:ui/src/engine.dart';
1111
import 'package:ui/ui.dart' as ui;
1212
import 'package:web_engine_tester/golden_tester.dart';
1313

14+
import '../common/rendering.dart';
1415
import '../common/test_initialization.dart';
1516

17+
export '../common/rendering.dart' show renderScene;
18+
1619
const MethodCodec codec = StandardMethodCodec();
1720

1821
/// Common test setup for all CanvasKit unit-tests.
@@ -44,13 +47,10 @@ CkPicture paintPicture(
4447

4548
Future<void> matchSceneGolden(
4649
String goldenFile,
47-
LayerScene scene, {
50+
ui.Scene scene, {
4851
required ui.Rect region,
4952
}) async {
50-
// TODO(harryterkelsen): Enforce the render rule. Render can only be called in
51-
// the scope of `onBeginFrame` or `onDrawFrame`,
52-
// https://github.com/flutter/flutter/issues/137073.
53-
CanvasKitRenderer.instance.renderScene(scene, implicitView);
53+
await renderScene(scene);
5454
await matchGoldenFile(goldenFile, region: region);
5555
}
5656

@@ -63,7 +63,7 @@ Future<void> matchPictureGolden(String goldenFile, CkPicture picture,
6363
final LayerSceneBuilder sb = LayerSceneBuilder();
6464
sb.pushOffset(0, 0);
6565
sb.addPicture(ui.Offset.zero, picture);
66-
CanvasKitRenderer.instance.renderScene(sb.build(), implicitView);
66+
await renderScene(sb.build());
6767
await matchGoldenFile(goldenFile, region: region);
6868
}
6969

0 commit comments

Comments
 (0)