Reland "Use a single OffscreenCanvas for rendering in CanvasKit (#45744)" (flutter/engine#47241)

Using a single GL context avoids several issues with managing GL context
lifecycle.

## 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 2023-10-24 13:21:58 -07:00 committed by GitHub
parent 5e760248e0
commit 84e466b785
20 changed files with 820 additions and 866 deletions

View File

@ -2621,10 +2621,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
@ -5400,10 +5401,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

View File

@ -43,10 +43,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';

View File

@ -161,6 +161,13 @@ extension CanvasKitExtension on CanvasKit {
DomCanvasElement canvas, SkWebGLContextOptions options) =>
_GetWebGLContext(canvas, options).toDartDouble;
@JS('GetWebGLContext')
external JSNumber _GetOffscreenWebGLContext(
DomOffscreenCanvas canvas, SkWebGLContextOptions options);
double GetOffscreenWebGLContext(
DomOffscreenCanvas canvas, SkWebGLContextOptions options) =>
_GetOffscreenWebGLContext(canvas, options).toDartDouble;
@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].

View File

@ -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';
@ -18,9 +17,9 @@ import 'embedded_views_diff.dart';
import 'path.dart';
import 'picture.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 {
@ -31,42 +30,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();
@ -86,10 +49,12 @@ class HtmlViewEmbedder {
/// * The number of clipping elements used last time the view was composited.
final Map<int, ViewClipChain> _viewClipChains = <int, ViewClipChain>{};
/// 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<int, Surface> _overlays = <int, Surface>{};
/// 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<int, RenderCanvas> _overlays = <int, RenderCanvas>{};
/// The views that need to be recomposited into the scene on the next frame.
final Set<int> _viewsToRecomposite = <int>{};
@ -100,6 +65,9 @@ class HtmlViewEmbedder {
/// The most recent composition order.
final List<int> _activeCompositionOrder = <int>[];
/// The most recent overlay groups.
List<OverlayGroup> _activeOverlayGroups = <OverlayGroup>[];
/// The size of the frame, in physical pixels.
ui.Size _frameSize = ui.window.physicalSize;
@ -124,20 +92,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.instance.isVisible(viewId);
if (needNewOverlay && hasAvailableOverlay) {
if (needNewOverlay) {
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _frameSize);
_context.pictureRecordersCreatedDuringPreroll.add(pictureRecorder);
@ -409,26 +367,27 @@ class HtmlViewEmbedder {
(_activeCompositionOrder.isEmpty || _compositionOrder.isEmpty)
? null
: diffViewList(_activeCompositionOrder, _compositionOrder);
_updateOverlays(diffResult);
final List<OverlayGroup>? overlayGroups = _updateOverlays(diffResult);
if (overlayGroups != null) {
_activeOverlayGroups = overlayGroups;
}
assert(
_context.pictureRecorders.length == _overlays.length,
'There should be the same number of picture recorders '
_context.pictureRecorders.length >= _overlays.length,
'There should be 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) {
final SurfaceFrame frame = _overlays[viewId]!.acquireFrame(_frameSize);
final CkCanvas canvas = frame.skiaCanvas;
final CkPicture ckPicture =
_context.pictureRecorders[pictureRecorderIndex].endRecording();
canvas.clear(const ui.Color(0x00000000));
canvas.drawPicture(ckPicture);
int pictureRecorderIndex = 0;
for (final OverlayGroup overlayGroup in _activeOverlayGroups) {
final RenderCanvas overlay = _overlays[overlayGroup.last]!;
final List<CkPicture> pictures = <CkPicture>[];
for (int i = 0; i < overlayGroup.visibleCount; i++) {
pictures.add(
_context.pictureRecorders[pictureRecorderIndex].endRecording());
pictureRecorderIndex++;
frame.submit();
}
CanvasKitRenderer.instance.rasterizer
.rasterizeToCanvas(overlay, pictures);
}
for (final CkPictureRecorder recorder
in _context.pictureRecordersCreatedDuringPreroll) {
@ -481,7 +440,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);
@ -489,7 +448,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);
}
@ -514,7 +473,7 @@ class HtmlViewEmbedder {
}
}
} else {
SurfaceFactory.instance.removeSurfacesFromDom();
RenderCanvasFactory.instance.removeSurfacesFromDom();
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
@ -532,7 +491,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);
@ -568,8 +527,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);
}
}
@ -591,13 +550,13 @@ class HtmlViewEmbedder {
// composition order of the current and previous frame, respectively.
//
// TODO(hterkelsen): Test this more thoroughly.
void _updateOverlays(ViewListDiffResult? diffResult) {
List<OverlayGroup>? _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
@ -606,17 +565,10 @@ class HtmlViewEmbedder {
getOverlayGroups(_compositionOrder);
final List<int> 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 > SurfaceFactory.instance.maximumOverlays) {
assert(viewsNeedingOverlays.length ==
SurfaceFactory.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 {
@ -635,6 +587,7 @@ class HtmlViewEmbedder {
.forEach(_initializeOverlay);
}
assert(_overlays.length == viewsNeedingOverlays.length);
return overlayGroups;
}
// Group the platform views into "overlay groups". These are sublists
@ -646,12 +599,8 @@ class HtmlViewEmbedder {
// be assigned an overlay are grouped together and will be rendered on top of
// the rest of the scene.
List<OverlayGroup> getOverlayGroups(List<int> views) {
final int maxOverlays = SurfaceFactory.instance.maximumOverlays;
if (maxOverlays == 0) {
return const <OverlayGroup>[];
}
final List<OverlayGroup> result = <OverlayGroup>[];
OverlayGroup currentGroup = OverlayGroup(<int>[]);
OverlayGroup currentGroup = OverlayGroup();
for (int i = 0; i < views.length; i++) {
final int view = views[i];
@ -660,8 +609,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
@ -671,17 +622,8 @@ 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(<int>[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();
currentGroup.add(view, visible: true);
}
}
}
@ -696,8 +638,7 @@ class HtmlViewEmbedder {
assert(!_overlays.containsKey(viewId));
// Try reusing a cached overlay created for another platform view.
final Surface overlay = SurfaceFactory.instance.getSurface()!;
overlay.createOrUpdateSurface(_frameSize);
final RenderCanvas overlay = RenderCanvasFactory.instance.getCanvas();
_overlays[viewId] = overlay;
}
@ -742,29 +683,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<int> viewGroup, {
bool visible = false,
}) : _group = viewGroup,
_containsVisibleView = visible;
OverlayGroup() : _group = <int>[];
// The internal list of ints.
final List<int> _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).

View File

@ -11,8 +11,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 ScenePicture {
@ -99,7 +99,7 @@ class CkPicture implements ScenePicture {
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();

View File

@ -3,22 +3,29 @@
// 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 '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<ui.VoidCallback> _postFrameCallbacks = <ui.VoidCallback>[];
/// 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();
ui.Size _currentFrameSize = ui.Size.zero;
/// Render the given [pictures] so it is displayed by the given [canvas].
Future<void> rasterizeToCanvas(
RenderCanvas canvas, List<CkPicture> pictures) async {
await _offscreenSurface.rasterizeToCanvas(
_currentFrameSize, canvas, pictures);
}
/// Sets the maximum size of the Skia resource cache, in bytes.
void setSkiaResourceCacheMaxBytes(int bytes) =>
SurfaceFactory.instance.baseSurface.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,17 +36,22 @@ class Rasterizer {
return;
}
final SurfaceFrame frame =
SurfaceFactory.instance.baseSurface.acquireFrame(layerTree.frameSize);
HtmlViewEmbedder.instance.frameSize = layerTree.frameSize;
final CkCanvas canvas = frame.skiaCanvas;
canvas.clear(const ui.Color(0x00000000));
final Frame compositorFrame =
context.acquireFrame(canvas, HtmlViewEmbedder.instance);
_currentFrameSize = layerTree.frameSize;
_offscreenSurface.acquireFrame(_currentFrameSize);
HtmlViewEmbedder.instance.frameSize = _currentFrameSize;
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _currentFrameSize);
pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000));
final Frame compositorFrame = context.acquireFrame(
pictureRecorder.recordingCanvas!, HtmlViewEmbedder.instance);
compositorFrame.raster(layerTree, ignoreRasterCache: true);
SurfaceFactory.instance.baseSurface.addToScene();
frame.submit();
CanvasKitRenderer.instance.sceneHost!
.prepend(RenderCanvasFactory.instance.baseCanvas.htmlElement);
rasterizeToCanvas(RenderCanvasFactory.instance.baseCanvas,
<CkPicture>[pictureRecorder.endRecording()]);
HtmlViewEmbedder.instance.submitFrame();
} finally {
_runPostFrameCallbacks();

View File

@ -0,0 +1,113 @@
// 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 '../dom.dart';
import '../window.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() {
canvasElement.setAttribute('aria-hidden', 'true');
canvasElement.style.position = 'absolute';
_updateLogicalHtmlCanvasSize();
htmlElement.append(canvasElement);
}
/// 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 `<canvas>` element used to display the pixels.
final DomCanvasElement canvasElement = createDomCanvasElement();
int _pixelWidth = 0;
int _pixelHeight = 0;
late final DomCanvasRenderingContextBitmapRenderer renderContext =
canvasElement.contextBitmapRenderer;
double _currentDevicePixelRatio = -1;
/// 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';
_currentDevicePixelRatio = window.devicePixelRatio;
}
/// Render the given [bitmap] with this [RenderCanvas].
///
/// 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) {
// Check if the frame is the same size as before, and if so, we don't need
// to resize the canvas.
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;
}
// 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.
_pixelWidth = size.width.ceil();
_pixelHeight = size.height.ceil();
canvasElement.width = _pixelWidth.toDouble();
canvasElement.height = _pixelHeight.toDouble();
_updateLogicalHtmlCanvasSize();
}
void dispose() {
htmlElement.remove();
}
}

View File

@ -0,0 +1,142 @@
// 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';
/// 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<RenderCanvas> _liveCanvases = <RenderCanvas>[];
/// Canvases created by this factory which are no longer in use. These can be
/// reused.
final List<RenderCanvas> _cache = <RenderCanvas>[];
/// 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;
}
}

View File

@ -11,11 +11,10 @@ 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 'picture.dart';
import 'render_canvas.dart';
import 'util.dart';
// Only supported in profile/release mode. Allows Flutter to use MSAA but
@ -26,8 +25,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;
@ -82,19 +80,16 @@ 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 OffscreenCanvas element used for this surface.
DomOffscreenCanvas? _offscreenCanvas;
/// Returns the underlying OffscreenCanvas. Should only be used in tests.
DomOffscreenCanvas? get debugOffscreenCanvas => _offscreenCanvas;
/// The <canvas> backing this Surface in the case that OffscreenCanvas isn't
/// supported.
DomCanvasElement? _canvasElement;
/// The underlying `<canvas>` element used for this surface.
DomCanvasElement? htmlCanvas;
int _pixelWidth = -1;
int _pixelHeight = -1;
int _sampleCount = -1;
@ -112,7 +107,31 @@ class Surface {
}
}
bool _addedToScene = false;
Future<void> rasterizeToCanvas(
ui.Size frameSize, RenderCanvas canvas, List<CkPicture> pictures) async {
final CkCanvas skCanvas = _surface!.getCanvas();
skCanvas.clear(const ui.Color(0x00000000));
pictures.forEach(skCanvas.drawPicture);
_surface!.flush();
DomImageBitmap bitmap;
if (Surface.offscreenCanvasSupported) {
bitmap = (await createImageBitmap(_offscreenCanvas!, (
x: 0,
y: _pixelHeight - frameSize.height.toInt(),
width: frameSize.width.toInt(),
height: frameSize.height.toInt(),
)).toDart)! as DomImageBitmap;
} else {
bitmap = (await createImageBitmap(_canvasElement!, (
x: 0,
y: _pixelHeight - frameSize.height.toInt(),
width: frameSize.width.toInt(),
height: frameSize.height.toInt()
)).toDart)! as DomImageBitmap;
}
canvas.render(bitmap);
}
/// Acquire a frame of the given [size] containing a drawable canvas.
///
@ -129,21 +148,16 @@ 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;
/// 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].
///
@ -159,22 +173,10 @@ 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);
}
/// 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 <canvas> and SkSurface for the given [size].
CkSurface createOrUpdateSurface(ui.Size size) {
if (size.isEmpty) {
@ -188,11 +190,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!;
}
@ -205,12 +202,16 @@ class Surface {
final ui.Size newSize = size * 1.4;
_surface?.dispose();
_surface = null;
htmlCanvas!.width = newSize.width;
htmlCanvas!.height = newSize.height;
if (Surface.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();
_updateLogicalHtmlCanvasSize();
}
}
@ -218,57 +219,20 @@ class Surface {
if (_forceNewContext || _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!;
}
/// 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 = htmlCanvas!.style;
style.width = '${logicalWidth}px';
style.height = '${logicalHeight}px';
}
/// Translate the canvas so the surface covers the visible portion of the
/// screen.
///
/// The <canvas> 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 <canvas>, and without translation,
/// only the top left of the <canvas> 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;
htmlCanvas!.style.transform = 'translate(0, -${offset}px)';
}
JSVoid _contextRestoredListener(DomEvent event) {
assert(
_contextLost,
@ -282,16 +246,11 @@ class Surface {
}
JSVoid _contextLostListener(DomEvent event) {
assert(event.target == htmlCanvas,
assert(event.target == _offscreenCanvas || event.target == _canvasElement,
'Received a context lost event for a disposed canvas');
final SurfaceFactory factory = SurfaceFactory.instance;
_contextLost = true;
if (factory.isLive(this)) {
_forceNewContext = true;
event.preventDefault();
} else {
dispose();
}
_forceNewContext = true;
event.preventDefault();
}
/// This function is expensive.
@ -299,18 +258,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 <canvas>.
if (this.htmlCanvas != null) {
this.htmlCanvas!.removeEventListener(
'webglcontextrestored',
_cachedContextRestoredListener,
false,
);
this.htmlCanvas!.removeEventListener(
'webglcontextlost',
_cachedContextLostListener,
false,
);
this.htmlCanvas!.remove();
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,
);
_canvasElement!.removeEventListener(
'webglcontextlost',
_cachedContextLostListener,
false,
);
_canvasElement = null;
_cachedContextRestoredListener = null;
_cachedContextLostListener = null;
}
@ -319,25 +292,22 @@ 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,
);
this.htmlCanvas = 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();
DomEventTarget htmlCanvas;
if (Surface.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;
}
// 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.
@ -345,7 +315,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',
@ -361,15 +332,24 @@ class Surface {
_contextLost = false;
if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) {
final int glContext = canvasKit.GetWebGLContext(
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 (Surface.offscreenCanvasSupported) {
glContext = canvasKit.GetOffscreenWebGLContext(
_offscreenCanvas!,
options,
).toInt();
} else {
glContext = canvasKit.GetWebGLContext(
_canvasElement!,
options,
).toInt();
}
_glContext = glContext;
@ -387,40 +367,38 @@ class Surface {
_syncCacheBytes();
}
}
htmlElement.append(htmlCanvas);
}
void _initWebglParams() {
final WebGLContext gl = htmlCanvas!.getGlContext(webGLVersion);
WebGLContext gl;
if (Surface.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(htmlCanvas != null);
assert(_offscreenCanvas != null || _canvasElement != null);
if (webGLVersion == -1) {
return _makeSoftwareCanvasSurface(
htmlCanvas!, 'WebGL support not detected');
return _makeSoftwareCanvasSurface('WebGL support not detected');
} else if (configuration.canvasKitForceCpuOnly) {
return _makeSoftwareCanvasSurface(
htmlCanvas!, 'CPU rendering forced by application');
return _makeSoftwareCanvasSurface('CPU rendering forced by application');
} else if (_glContext == 0) {
return _makeSoftwareCanvasSurface(
htmlCanvas!, 'Failed to initialize WebGL context');
return _makeSoftwareCanvasSurface('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(
htmlCanvas!, 'Failed to initialize WebGL surface');
return _makeSoftwareCanvasSurface('Failed to initialize WebGL surface');
}
return CkSurface(skSurface, _glContext);
@ -429,14 +407,20 @@ class Surface {
static bool _didWarnAboutWebGlInitializationFailure = false;
CkSurface _makeSoftwareCanvasSurface(
DomCanvasElement htmlCanvas, String reason) {
CkSurface _makeSoftwareCanvasSurface(String reason) {
if (!_didWarnAboutWebGlInitializationFailure) {
printWarning('WARNING: Falling back to CPU-only rendering. $reason.');
_didWarnAboutWebGlInitializationFailure = true;
}
SkSurface surface;
if (Surface.offscreenCanvasSupported) {
surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!);
} else {
surface = canvasKit.MakeSWCanvasSurface(_canvasElement!);
}
return CkSurface(
canvasKit.MakeSWCanvasSurface(htmlCanvas),
surface,
null,
);
}
@ -447,15 +431,19 @@ class Surface {
}
void dispose() {
htmlCanvas?.removeEventListener(
_offscreenCanvas?.removeEventListener(
'webglcontextlost', _cachedContextLostListener, false);
htmlCanvas?.removeEventListener(
_offscreenCanvas?.removeEventListener(
'webglcontextrestored', _cachedContextRestoredListener, false);
_cachedContextLostListener = null;
_cachedContextRestoredListener = null;
htmlElement.remove();
_surface?.dispose();
}
/// Safari 15 doesn't support OffscreenCanvas at all. Safari 16 supports
/// OffscreenCanvas, but only with the context2d API, not WebGL.
static bool get offscreenCanvasSupported =>
browserSupportsOffscreenCanvas && !isSafari;
}
/// A Dart wrapper around Skia's CkSurface.

View File

@ -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<Surface> _liveSurfaces = <Surface>[];
/// Surfaces created by this factory which are no longer in use. These can be
/// reused.
final List<Surface> _cache = <Surface>[];
/// 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;
}
}

View File

@ -257,15 +257,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 `<canvas>` 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(

View File

@ -198,6 +198,28 @@ external DomIntl get domIntl;
@JS('Symbol')
external DomSymbol get domSymbol;
@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 DomNavigator {}
@ -1407,7 +1429,7 @@ extension DomCanvasRenderingContextWebGlExtension
class DomCanvasRenderingContextBitmapRenderer {}
extension DomCanvasRenderingContextBitmapRendererExtension
on DomCanvasRenderingContextBitmapRenderer {
on DomCanvasRenderingContextBitmapRenderer {
external void transferFromImageBitmap(DomImageBitmap bitmap);
}
@ -1415,10 +1437,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')
@ -1436,33 +1461,6 @@ extension DomImageBitmapExtension on DomImageBitmap {
external void close();
}
@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 {}
@ -1505,7 +1503,8 @@ MockHttpFetchResponseFactory? mockHttpFetchResponseFactory;
/// [httpFetchText] instead.
Future<HttpFetchResponse> httpFetch(String url) async {
if (mockHttpFetchResponseFactory != null) {
final MockHttpFetchResponse? response = await mockHttpFetchResponseFactory!(url);
final MockHttpFetchResponse? response =
await mockHttpFetchResponseFactory!(url);
if (response != null) {
return response;
}
@ -1762,8 +1761,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;
}
@ -1773,10 +1771,12 @@ class MockHttpFetchPayload implements HttpFetchPayload {
Future<ByteBuffer> asByteBuffer() async => _byteBuffer;
@override
Future<dynamic> json() async => throw AssertionError('json not supported by mock');
Future<dynamic> json() async =>
throw AssertionError('json not supported by mock');
@override
Future<String> text() async => throw AssertionError('text not supported by mock');
Future<String> text() async =>
throw AssertionError('text not supported by mock');
}
/// Indicates a missing HTTP payload when one was expected, such as when
@ -2311,9 +2311,7 @@ DomBlob createDomBlob(List<Object?> parts, [Map<String, dynamic>? options]) {
return DomBlob(parts.toJSAnyShallow as JSArray);
} else {
return DomBlob.withOptions(
parts.toJSAnyShallow as JSArray,
options.toJSAnyDeep
);
parts.toJSAnyShallow as JSArray, options.toJSAnyDeep);
}
}
@ -2845,6 +2843,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')
@ -2858,6 +2863,11 @@ 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) =>
@ -3451,8 +3461,8 @@ class DomSegments {}
extension DomSegmentsExtension on DomSegments {
DomIteratorWrapper<DomSegment> iterator() {
final DomIterator segmentIterator =
js_util.callMethod(this, domSymbol.iterator, const <Object?>[]) as DomIterator;
final DomIterator segmentIterator = js_util
.callMethod(this, domSymbol.iterator, const <Object?>[]) as DomIterator;
return DomIteratorWrapper<DomSegment>(segmentIterator);
}
}
@ -3589,10 +3599,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,
<Object>[cleanup]
);
js_util.callConstructor(
_finalizationRegistryConstructor!.toObjectShallow, <Object>[cleanup]);
extension DomFinalizationRegistryExtension on DomFinalizationRegistry {
@JS('register')
@ -3601,11 +3609,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')
@ -3617,6 +3626,11 @@ extension DomFinalizationRegistryExtension on DomFinalizationRegistry {
bool browserSupportsFinalizationRegistry =
_finalizationRegistryConstructor != null;
@JS('window.OffscreenCanvas')
external JSAny? get _offscreenCanvasConstructor;
bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null;
@JS()
@staticInterop
extension JSArrayExtension on JSArray {

View File

@ -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',

View File

@ -24,7 +24,7 @@ void setUpCanvasKitTest() {
tearDown(() {
HtmlViewEmbedder.instance.debugClear();
SurfaceFactory.instance.debugClear();
RenderCanvasFactory.instance.debugClear();
});
setUp(() =>

View File

@ -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';
@ -42,10 +41,10 @@ void testMain() {
// The platform view is now split in two parts. The contents live
// as a child of the glassPane, and the slot lives in the glassPane
// shadow root. The slot is the one that has pointer events auto.
final DomElement contents = flutterViewEmbedder.glassPaneElement
.querySelector('#view-0')!;
final DomElement slot = flutterViewEmbedder.sceneElement!
.querySelector('slot')!;
final DomElement contents =
flutterViewEmbedder.glassPaneElement.querySelector('#view-0')!;
final DomElement slot =
flutterViewEmbedder.sceneElement!.querySelector('slot')!;
final DomElement contentsHost = contents.parent!;
final DomElement slotHost = slot.parent!;
@ -292,8 +291,7 @@ void testMain() {
});
test('renders overlays on top of platform views', () async {
expect(SurfaceFactory.instance.debugCacheSize, 0);
expect(configuration.canvasKitMaximumSurfaces, 8);
expect(RenderCanvasFactory.instance.debugCacheSize, 0);
final CkPicture testPicture =
paintPicture(const ui.Rect.fromLTRB(0, 0, 10, 10), (CkCanvas canvas) {
canvas.drawCircle(const ui.Offset(5, 5), 5, CkPaint());
@ -339,8 +337,8 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_overlay,
_platformView,
_overlay,
]);
// Frame 2:
@ -372,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<void>.delayed(Duration.zero);
renderTestScene(viewCount: 16);
@ -391,16 +389,16 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
_platformView,
]);
// Frame 5:
@ -477,7 +475,6 @@ void testMain() {
// Render: Views 1-10
// Expect: main canvas plus platform view overlays; empty cache.
renderTestScene(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(SurfaceFactory.instance.numAvailableOverlays, 0);
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
@ -493,10 +490,10 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
]);
// Frame 2:
@ -504,7 +501,6 @@ void testMain() {
// Expect: main canvas plus platform view overlays; empty cache.
await Future<void>.delayed(Duration.zero);
renderTestScene(<int>[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
expect(SurfaceFactory.instance.numAvailableOverlays, 0);
_expectSceneMatches(<_EmbeddedViewMarker>[
_overlay,
_platformView,
@ -520,10 +516,10 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
]);
// Frame 3:
@ -546,10 +542,10 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
]);
// Frame 4:
@ -572,10 +568,10 @@ void testMain() {
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
_platformView,
_overlay,
_platformView,
_platformView,
_platformView,
]);
// TODO(yjbanov): skipped due to https://github.com/flutter/flutter/issues/73867
@ -599,8 +595,7 @@ void testMain() {
]);
expect(
flutterViewEmbedder.glassPaneElement
.querySelector('flt-platform-view'),
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
isNotNull,
);
@ -615,13 +610,14 @@ void testMain() {
]);
expect(
flutterViewEmbedder.glassPaneElement
.querySelector('flt-platform-view'),
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
isNull,
);
});
test('does not crash when resizing the window after textures have been registered', () async {
test(
'does not crash when resizing the window after textures have been registered',
() async {
ui_web.platformViewRegistry.registerViewFactory(
'test-platform-view',
(int viewId) => createDomHTMLDivElement()..id = 'view-0',
@ -664,7 +660,7 @@ void testMain() {
window.debugPhysicalSizeOverride = null;
window.debugForceResize();
// ImageDecoder is not supported in Safari or Firefox.
// ImageDecoder is not supported in Safari or Firefox.
}, skip: isSafari || isFirefox);
test('removed the DOM node of an unrendered platform view', () async {
@ -686,8 +682,7 @@ void testMain() {
]);
expect(
flutterViewEmbedder.glassPaneElement
.querySelector('flt-platform-view'),
flutterViewEmbedder.glassPaneElement.querySelector('flt-platform-view'),
isNotNull,
);
@ -744,8 +739,8 @@ void testMain() {
rasterizer.draw(sb.build().layerTree);
}
final DomNode skPathDefs = flutterViewEmbedder.sceneElement!
.querySelector('#sk_path_defs')!;
final DomNode skPathDefs =
flutterViewEmbedder.sceneElement!.querySelector('#sk_path_defs')!;
expect(skPathDefs.childNodes, hasLength(0));
@ -782,121 +777,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;
debugOverrideJsConfiguration(
<String, Object?>{
'canvasKitMaximumSurfaces': 2,
}.jsify() as JsFlutterConfiguration?
);
expect(configuration.canvasKitMaximumSurfaces, 2);
expect(configuration.canvasKitVariant, isNot(CanvasKitVariant.auto));
SurfaceFactory.instance.debugClear();
expect(SurfaceFactory.instance.maximumSurfaces, 2);
expect(SurfaceFactory.instance.maximumOverlays, 1);
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
debugOverrideJsConfiguration(null);
});
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(
@ -957,7 +837,9 @@ void testMain() {
_overlay,
_platformView,
_overlay,
], reason: 'Overlays created after each group containing a visible view.');
],
reason:
'Overlays created after each group containing a visible view.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@ -1059,7 +941,9 @@ void testMain() {
_platformView,
_platformView,
_platformView,
], reason: 'Many invisible views can be rendered on top of the base overlay.');
],
reason:
'Many invisible views can be rendered on top of the base overlay.');
sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
@ -1108,19 +992,22 @@ enum _EmbeddedViewMarker {
_EmbeddedViewMarker get _overlay => _EmbeddedViewMarker.overlay;
_EmbeddedViewMarker get _platformView => _EmbeddedViewMarker.platformView;
const Map<String, _EmbeddedViewMarker> _tagToViewMarker = <String, _EmbeddedViewMarker>{
const Map<String, _EmbeddedViewMarker> _tagToViewMarker =
<String, _EmbeddedViewMarker>{
'flt-canvas-container': _EmbeddedViewMarker.overlay,
'flt-platform-view-slot': _EmbeddedViewMarker.platformView,
};
void _expectSceneMatches(List<_EmbeddedViewMarker> expectedMarkers, {
void _expectSceneMatches(
List<_EmbeddedViewMarker> expectedMarkers, {
String? reason,
}) {
// Convert the scene elements to its corresponding array of _EmbeddedViewMarker
final List<_EmbeddedViewMarker> sceneElements = flutterViewEmbedder
.sceneElement!.children
.where((DomElement element) => element.tagName != 'svg')
.map((DomElement element) => _tagToViewMarker[element.tagName.toLowerCase()]!)
.map((DomElement element) =>
_tagToViewMarker[element.tagName.toLowerCase()]!)
.toList();
expect(sceneElements, expectedMarkers, reason: reason);

View File

@ -0,0 +1,95 @@
// 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 '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
CanvasKitRenderer.instance.sceneHost!
.prepend(originalFactory.baseCanvas.htmlElement);
expect(originalFactory.baseCanvas.canvasElement.isConnected, isTrue);
// Create a few overlay canvases
final List<RenderCanvas> overlays = <RenderCanvas>[];
for (int i = 0; i < 3; i++) {
final RenderCanvas canvas = originalFactory.getCanvas();
CanvasKitRenderer.instance.sceneHost!.prepend(canvas.htmlElement);
overlays.add(canvas);
}
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);
});
});
}

View File

@ -0,0 +1,63 @@
// 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:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('CanvasKit', () {
setUpCanvasKitTest();
setUp(() async {
window.debugOverrideDevicePixelRatio(1.0);
});
Future<DomImageBitmap> newBitmap(int width, int height) async {
return (await createImageBitmap(
createBlankDomImageData(width, height) as JSAny, (
x: 0,
y: 0,
width: width,
height: height,
)).toDart)! as DomImageBitmap;
}
// Regression test for https://github.com/flutter/flutter/issues/75286
test('updates canvas logical size when device-pixel ratio changes',
() async {
final RenderCanvas canvas = RenderCanvas();
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');
// 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.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.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');
});
});
}

View File

@ -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.htmlCanvas!.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.htmlCanvas!.isConnected, isTrue);
// Create a few overlay surfaces
final List<Surface> overlays = <Surface>[];
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);
});
});
}

View File

@ -23,17 +23,14 @@ void testMain() {
});
test('Surface allocates canvases efficiently', () {
final Surface? surface = SurfaceFactory.instance.getSurface();
final Surface surface = Surface();
final CkSurface originalSurface =
surface!.acquireFrame(const ui.Size(9, 19)).skiaSurface;
final DomCanvasElement original = surface.htmlCanvas!;
surface.acquireFrame(const ui.Size(9, 19)).skiaSurface;
final DomOffscreenCanvas original = surface.debugOffscreenCanvas!;
// 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.htmlCanvas!;
final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!;
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.htmlCanvas!;
final DomOffscreenCanvas firstIncrease = surface.debugOffscreenCanvas!;
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.htmlCanvas!;
final DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!;
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.htmlCanvas!;
final DomOffscreenCanvas huge = surface.debugOffscreenCanvas!;
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.htmlCanvas!;
final DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!;
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.htmlCanvas!;
final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!;
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);
@ -123,13 +104,13 @@ 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',
() async {
final Surface? surface = SurfaceFactory.instance.getSurface();
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.debugOffscreenCanvas!;
final Object ctx = canvas.getContext('webgl2')!;
final Object loseContextExtension = js_util.callMethod(
ctx,
@ -172,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
@ -183,9 +163,8 @@ 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.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.
@ -194,9 +173,8 @@ 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.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.
@ -205,9 +183,8 @@ 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.debugOffscreenCanvas!.width, 10);
expect(surface.debugOffscreenCanvas!.height, 16);
// See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172
window.debugOverrideDevicePixelRatio(2.0);
@ -215,28 +192,10 @@ 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.debugOffscreenCanvas!.width, 10);
expect(surface.debugOffscreenCanvas!.height, 16);
},
skip: !Surface.offscreenCanvasSupported,
);
});
}
/// 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.
);
}
}

View File

@ -19,9 +19,8 @@ void main() {
}
class StubPictureRenderer implements PictureRenderer {
final DomCanvasElement scratchCanvasElement = createDomCanvasElement(
width: 500, height: 500
);
final DomCanvasElement scratchCanvasElement =
createDomCanvasElement(width: 500, height: 500);
@override
Future<DomImageBitmap> renderPicture(ScenePicture picture) async {
@ -63,9 +62,11 @@ void testMain() {
final List<DomElement> 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<DomElement> containerChildren = containerElement.children.toList();
final List<DomElement> containerChildren =
containerElement.children.toList();
expect(containerChildren.length, 1);
final DomElement canvasElement = containerChildren.first;
final DomCSSStyleDeclaration style = canvasElement.style;
@ -81,12 +82,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>[platformView], null));
final EngineScene scene = EngineScene(rootLayer);
@ -96,7 +96,8 @@ void testMain() {
final List<DomElement> 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');