Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 98 additions & 29 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ class HtmlViewEmbedder {
/// The root view in the stack of mutator elements for the view id.
final Map<int?, html.Element?> _rootViews = <int?, html.Element?>{};

/// The overlay for the view id.
final Map<int, Overlay> _overlays = <int, Overlay>{};
/// 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 views that need to be recomposited into the scene on the next frame.
final Set<int> _viewsToRecomposite = <int>{};

/// The views that need to be disposed of on the next frame.
final Set<int?> _viewsToDispose = <int?>{};
final Set<int> _viewsToDispose = <int>{};

/// The list of view ids that should be composited, in order.
List<int> _compositionOrder = <int>[];
Expand Down Expand Up @@ -115,14 +117,15 @@ class HtmlViewEmbedder {

void _dispose(
MethodCall methodCall, ui.PlatformMessageResponseCallback callback) {
int? viewId = methodCall.arguments;
final int? viewId = methodCall.arguments;
const MethodCodec codec = StandardMethodCodec();
if (!_views.containsKey(viewId)) {
if (viewId == null || !_views.containsKey(viewId)) {
callback(codec.encodeErrorEnvelope(
code: 'unknown_view',
message: 'trying to dispose an unknown view',
details: 'view id: $viewId',
));
return;
}
_viewsToDispose.add(viewId);
callback(codec.encodeSuccessEnvelope(null));
Expand Down Expand Up @@ -339,9 +342,9 @@ class HtmlViewEmbedder {

for (int i = 0; i < _compositionOrder.length; i++) {
int viewId = _compositionOrder[i];
ensureOverlayInitialized(viewId);
_ensureOverlayInitialized(viewId);
final SurfaceFrame frame =
_overlays[viewId]!.surface.acquireFrame(_frameSize);
_overlays[viewId]!.acquireFrame(_frameSize);
final CkCanvas canvas = frame.skiaCanvas;
canvas.drawPicture(
_pictureRecorders[viewId]!.endRecording(),
Expand All @@ -353,53 +356,127 @@ class HtmlViewEmbedder {
_compositionOrder.clear();
return;
}

final Set<int> unusedViews = Set<int>.from(_activeCompositionOrder);
_activeCompositionOrder.clear();

for (int i = 0; i < _compositionOrder.length; i++) {
int viewId = _compositionOrder[i];

assert(
_views.containsKey(viewId),
'Cannot render platform view $viewId. '
'It has not been created, or it has been deleted.',
);

unusedViews.remove(viewId);
html.Element platformViewRoot = _rootViews[viewId]!;
html.Element overlay = _overlays[viewId]!.surface.htmlElement!;
html.Element overlay = _overlays[viewId]!.htmlElement;
platformViewRoot.remove();
skiaSceneHost!.append(platformViewRoot);
overlay.remove();
skiaSceneHost!.append(overlay);
_activeCompositionOrder.add(viewId);
}
_compositionOrder.clear();

for (final int unusedViewId in unusedViews) {
_releaseOverlay(unusedViewId);
}
}

void disposeViews() {
if (_viewsToDispose.isEmpty) {
return;
}

for (int? viewId in _viewsToDispose) {
for (final int viewId in _viewsToDispose) {
final html.Element rootView = _rootViews[viewId]!;
rootView.remove();
_views.remove(viewId);
_rootViews.remove(viewId);
if (_overlays[viewId] != null) {
final Overlay overlay = _overlays[viewId]!;
overlay.surface.htmlElement?.remove();
overlay.surface.htmlElement = null;
overlay.skSurface?.dispose();
}
_overlays.remove(viewId);
_releaseOverlay(viewId);
_currentCompositionParams.remove(viewId);
_clipCount.remove(viewId);
_viewsToRecomposite.remove(viewId);
}
_viewsToDispose.clear();
}

void ensureOverlayInitialized(int viewId) {
Overlay? overlay = _overlays[viewId];
void _releaseOverlay(int viewId) {
if (_overlays[viewId] != null) {
OverlayCache.instance.releaseOverlay(_overlays[viewId]!);
_overlays.remove(viewId);
}
}

void _ensureOverlayInitialized(int viewId) {
// If there's an active overlay for the view ID, continue using it.
Surface? overlay = _overlays[viewId];
if (overlay != null) {
return;
}
Surface surface = Surface(this);
CkSurface? skSurface = surface.acquireRenderSurface(_frameSize);
_overlays[viewId] = Overlay(surface, skSurface);

// Try reusing a cached overlay created for another platform view.
overlay = OverlayCache.instance.reserveOverlay();

// If nothing to reuse, create a new overlay.
if (overlay == null) {
overlay = Surface(this);
}

_overlays[viewId] = overlay;
}
}

/// Caches surfaces used to overlay platform views.
class OverlayCache {
static const int kDefaultCacheSize = 5;

/// The cache singleton.
static final OverlayCache instance = OverlayCache(kDefaultCacheSize);

OverlayCache(this.maximumSize);

/// The cache will not grow beyond this size.
final int maximumSize;

/// Cached surfaces, available for reuse.
final List<Surface> _cache = <Surface>[];

/// Returns the list of cached surfaces.
///
/// Useful in tests.
List<Surface> get debugCachedSurfaces => _cache;

/// Reserves an overlay from the cache, if available.
///
/// Returns null if the cache is empty.
Surface? reserveOverlay() {
if (_cache.isEmpty) {
return null;
}
return _cache.removeLast();
}

/// Returns an overlay back to the cache.
///
/// If the cache is full, the overlay is deleted.
void releaseOverlay(Surface overlay) {
overlay.htmlElement.remove();
if (_cache.length < maximumSize) {
_cache.add(overlay);
} else {
overlay.dispose();
}
}

int get debugLength => _cache.length;

void debugClear() {
for (final Surface overlay in _cache) {
overlay.dispose();
}
}
}

Expand Down Expand Up @@ -547,11 +624,3 @@ class MutatorsStack extends Iterable<Mutator> {
@override
Iterator<Mutator> get iterator => _mutators.reversed.iterator;
}

/// Represents a surface overlaying a platform view.
class Overlay {
final Surface surface;
final CkSurface? skSurface;

Overlay(this.surface, this.skSurface);
}
84 changes: 63 additions & 21 deletions lib/web_ui/lib/src/engine/canvaskit/surface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,27 @@ class Surface {
Surface(this.viewEmbedder);

CkSurface? _surface;
html.Element? htmlElement;

/// If true, forces a new WebGL context to be created, even if the window
/// size is the same. This is used to restore the UI after the browser tab
/// goes dormant and loses the GL context.
bool _forceNewContext = true;
bool get debugForceNewContext => _forceNewContext;

SkGrContext? _grContext;
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 html.Element htmlElement = html.Element.tag('flt-canvas-container');

/// Specify the GPU resource cache limits.
void setSkiaResourceCacheMaxBytes(int bytes) {
_skiaCacheBytes = bytes;
Expand All @@ -64,7 +81,7 @@ class Surface {
///
/// The given [size] is in physical pixels.
SurfaceFrame acquireFrame(ui.Size size) {
final CkSurface surface = acquireRenderSurface(size);
final CkSurface surface = _createOrUpdateSurfaces(size);

if (surface.context != null) {
canvasKit.setCurrentContext(surface.context!);
Expand All @@ -77,33 +94,29 @@ class Surface {
return SurfaceFrame(surface, submitCallback);
}

CkSurface acquireRenderSurface(ui.Size size) {
_createOrUpdateSurfaces(size);
return _surface!;
}

void addToScene() {
if (!_addedToScene) {
skiaSceneHost!.children.insert(0, htmlElement!);
skiaSceneHost!.children.insert(0, htmlElement);
}
_addedToScene = true;
}

ui.Size? _currentSize;

void _createOrUpdateSurfaces(ui.Size size) {
CkSurface _createOrUpdateSurfaces(ui.Size size) {
if (size.isEmpty) {
throw CanvasKitError('Cannot create surfaces of empty size.');
}

// Check if the window is shrinking in size, and if so, don't allocate a
// new canvas as the previous canvas is big enough to fit everything.
final ui.Size? previousSize = _currentSize;
if (previousSize != null &&
if (!_forceNewContext &&
previousSize != null &&
size.width <= previousSize.width &&
size.height <= previousSize.height) {
// The existing surface is still reusable.
return;
return _surface!;
}

_currentSize = _currentSize == null
Expand All @@ -116,14 +129,17 @@ class Surface {

_surface?.dispose();
_surface = null;
htmlElement?.remove();
htmlElement = null;
_addedToScene = false;

_surface = _wrapHtmlCanvas(_currentSize!);
return _surface = _wrapHtmlCanvas(_currentSize!);
}

CkSurface _wrapHtmlCanvas(ui.Size physicalSize) {
// Clear the container, if it's not empty.
while (htmlElement.firstChild != null) {
htmlElement.firstChild!.remove();
}

// If `physicalSize` is not precise, use a slightly bigger canvas. This way
// we ensure that the rendred picture covers the entire browser window.
final int pixelWidth = physicalSize.width.ceil();
Expand All @@ -146,9 +162,28 @@ class Surface {
..width = '${logicalWidth}px'
..height = '${logicalHeight}px';

htmlElement = htmlCanvas;
if (webGLVersion == -1 || canvasKitForceCpuOnly) {
return _makeSoftwareCanvasSurface(htmlCanvas);
// 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.
// When this happens, the browser sends the "webglcontextlost" event as a
// notification. When we receive this notification we force a new context.
//
// See also: https://www.khronos.org/webgl/wiki/HandlingContextLost
htmlCanvas.addEventListener('webglcontextlost', (event) {
print('Flutter: restoring WebGL context.');
_forceNewContext = true;
// Force the framework to rerender the frame.
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
event.stopPropagation();
event.preventDefault();
}, false);
_forceNewContext = false;

htmlElement.append(htmlCanvas);

if (webGLVersion == -1) {
return _makeSoftwareCanvasSurface(htmlCanvas, 'WebGL support not detected');
} else if (canvasKitForceCpuOnly) {
return _makeSoftwareCanvasSurface(htmlCanvas, 'CPU rendering forced by application');
} else {
// Try WebGL first.
final int glContext = canvasKit.GetWebGLContext(
Expand All @@ -162,7 +197,7 @@ class Surface {
);

if (glContext == 0) {
return _makeSoftwareCanvasSurface(htmlCanvas);
return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL context');
}

_grContext = canvasKit.MakeGrContext(glContext);
Expand All @@ -183,7 +218,7 @@ class Surface {
);

if (skSurface == null) {
return _makeSoftwareCanvasSurface(htmlCanvas);
return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL surface');
}

return CkSurface(skSurface, _grContext, glContext);
Expand All @@ -192,9 +227,11 @@ class Surface {

static bool _didWarnAboutWebGlInitializationFailure = false;

CkSurface _makeSoftwareCanvasSurface(html.CanvasElement htmlCanvas) {
CkSurface _makeSoftwareCanvasSurface(html.CanvasElement htmlCanvas, String reason) {
if (!_didWarnAboutWebGlInitializationFailure) {
html.window.console.warn('WARNING: failed to initialize WebGL. Falling back to CPU-only rendering.');
html.window.console.warn(
'WARNING: Falling back to CPU-only rendering. $reason.'
);
_didWarnAboutWebGlInitializationFailure = true;
}
return CkSurface(
Expand All @@ -211,6 +248,11 @@ class Surface {
_surface!.flush();
return true;
}

void dispose() {
htmlElement.remove();
_surface?.dispose();
}
}

/// A Dart wrapper around Skia's CkSurface.
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/test/canvaskit/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void setUpCanvasKitTest() {
tearDown(() {
testCollector.cleanUpAfterTest();
debugResetBrowserSupportsFinalizationRegistry();
OverlayCache.instance.debugClear();
});

tearDownAll(() {
Expand Down
Loading