mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[canvaskit] cache and reuse platform view overlays (#23061)
This commit is contained in:
parent
3d3e16e303
commit
3d4c021fbd
@ -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>[];
|
||||
@ -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));
|
||||
@ -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(),
|
||||
@ -353,12 +356,22 @@ 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();
|
||||
@ -366,6 +379,10 @@ class HtmlViewEmbedder {
|
||||
_activeCompositionOrder.add(viewId);
|
||||
}
|
||||
_compositionOrder.clear();
|
||||
|
||||
for (final int unusedViewId in unusedViews) {
|
||||
_releaseOverlay(unusedViewId);
|
||||
}
|
||||
}
|
||||
|
||||
void disposeViews() {
|
||||
@ -373,18 +390,12 @@ class HtmlViewEmbedder {
|
||||
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);
|
||||
@ -392,14 +403,80 @@ class HtmlViewEmbedder {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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!);
|
||||
@ -77,21 +94,16 @@ 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.');
|
||||
}
|
||||
@ -99,11 +111,12 @@ class Surface {
|
||||
// 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
|
||||
@ -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();
|
||||
@ -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(
|
||||
@ -162,7 +197,7 @@ class Surface {
|
||||
);
|
||||
|
||||
if (glContext == 0) {
|
||||
return _makeSoftwareCanvasSurface(htmlCanvas);
|
||||
return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL context');
|
||||
}
|
||||
|
||||
_grContext = canvasKit.MakeGrContext(glContext);
|
||||
@ -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);
|
||||
@ -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(
|
||||
@ -211,6 +248,11 @@ class Surface {
|
||||
_surface!.flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
htmlElement.remove();
|
||||
_surface?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// A Dart wrapper around Skia's CkSurface.
|
||||
|
||||
@ -38,6 +38,7 @@ void setUpCanvasKitTest() {
|
||||
tearDown(() {
|
||||
testCollector.cleanUpAfterTest();
|
||||
debugResetBrowserSupportsFinalizationRegistry();
|
||||
OverlayCache.instance.debugClear();
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
|
||||
@ -114,6 +114,110 @@ void testMain() {
|
||||
'matrix3d(5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 515, 515, 0, 1)',
|
||||
);
|
||||
});
|
||||
|
||||
test('renders overlays on top of platform views', () async {
|
||||
expect(OverlayCache.instance.debugLength, 0);
|
||||
final CkPicture testPicture = paintPicture(
|
||||
ui.Rect.fromLTRB(0, 0, 10, 10),
|
||||
(CkCanvas canvas) {
|
||||
canvas.drawCircle(ui.Offset(5, 5), 5, CkPaint());
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize all platform views to be used in the test.
|
||||
final List<int> platformViewIds = <int>[];
|
||||
for (int i = 0; i < OverlayCache.kDefaultCacheSize * 2; i++) {
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
'test-platform-view',
|
||||
(viewId) => html.DivElement()..id = 'view-$i',
|
||||
);
|
||||
await _createPlatformView(i, 'test-platform-view');
|
||||
platformViewIds.add(i);
|
||||
}
|
||||
|
||||
final EnginePlatformDispatcher dispatcher =
|
||||
ui.window.platformDispatcher as EnginePlatformDispatcher;
|
||||
|
||||
void renderTestScene({ required int viewCount }) {
|
||||
LayerSceneBuilder sb = LayerSceneBuilder();
|
||||
sb.pushOffset(0, 0);
|
||||
for (int i = 0; i < viewCount; i++) {
|
||||
sb.addPicture(ui.Offset.zero, testPicture);
|
||||
sb.addPlatformView(i, width: 10, height: 10);
|
||||
}
|
||||
dispatcher.rasterizer!.draw(sb.build().layerTree);
|
||||
}
|
||||
|
||||
int countCanvases() {
|
||||
return domRenderer.sceneElement!.querySelectorAll('canvas').length;
|
||||
}
|
||||
|
||||
// Frame 1:
|
||||
// Render: up to cache size platform views.
|
||||
// Expect: main canvas plus platform view overlays; empty cache.
|
||||
renderTestScene(viewCount: OverlayCache.kDefaultCacheSize);
|
||||
expect(countCanvases(), OverlayCache.kDefaultCacheSize + 1);
|
||||
expect(OverlayCache.instance.debugLength, 0);
|
||||
|
||||
// Frame 2:
|
||||
// Render: zero platform views.
|
||||
// Expect: main canvas, no overlays; overlays in the cache.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(viewCount: 0);
|
||||
expect(countCanvases(), 1);
|
||||
expect(OverlayCache.instance.debugLength, 5);
|
||||
|
||||
// Frame 3:
|
||||
// Render: less than cache size platform views.
|
||||
// Expect: overlays reused; cache shrinks.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(viewCount: OverlayCache.kDefaultCacheSize - 2);
|
||||
expect(countCanvases(), OverlayCache.kDefaultCacheSize - 1);
|
||||
expect(OverlayCache.instance.debugLength, 2);
|
||||
|
||||
// Frame 4:
|
||||
// Render: more platform views than max cache size.
|
||||
// Expect: cache empty (everything reused).
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(viewCount: OverlayCache.kDefaultCacheSize * 2);
|
||||
expect(countCanvases(), OverlayCache.kDefaultCacheSize * 2 + 1);
|
||||
expect(OverlayCache.instance.debugLength, 0);
|
||||
|
||||
// Frame 5:
|
||||
// Render: zero platform views.
|
||||
// Expect: main canvas, no overlays; cache full but does not exceed limit.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
renderTestScene(viewCount: 0);
|
||||
expect(countCanvases(), 1);
|
||||
expect(OverlayCache.instance.debugLength, 5);
|
||||
|
||||
// Frame 6:
|
||||
// Render: deleted platform views.
|
||||
// Expect: error.
|
||||
for (final int id in platformViewIds) {
|
||||
final codec = StandardMethodCodec();
|
||||
final Completer<void> completer = Completer<void>();
|
||||
ui.window.sendPlatformMessage(
|
||||
'flutter/platform_views',
|
||||
codec.encodeMethodCall(MethodCall(
|
||||
'dispose',
|
||||
id,
|
||||
)),
|
||||
completer.complete,
|
||||
);
|
||||
await completer.future;
|
||||
}
|
||||
|
||||
try {
|
||||
renderTestScene(viewCount: platformViewIds.length);
|
||||
fail('Expected to throw');
|
||||
} on AssertionError catch (error) {
|
||||
expect(
|
||||
error.toString(),
|
||||
'Assertion failed: "Cannot render platform view 0. It has not been created, or it has been deleted."',
|
||||
);
|
||||
}
|
||||
});
|
||||
// TODO: https://github.com/flutter/flutter/issues/60040
|
||||
}, skip: isIosSafari);
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.12
|
||||
import 'dart:html' as html;
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
@ -20,19 +22,19 @@ void testMain() {
|
||||
|
||||
test('Surface allocates canvases efficiently', () {
|
||||
final Surface surface = Surface(HtmlViewEmbedder());
|
||||
final CkSurface original = surface.acquireRenderSurface(ui.Size(9, 19));
|
||||
final CkSurface original = surface.acquireFrame(ui.Size(9, 19)).skiaSurface;
|
||||
|
||||
// Expect exact requested dimensions.
|
||||
expect(original.width(), 9);
|
||||
expect(original.height(), 19);
|
||||
|
||||
// Shrinking reuses the existing surface straight-up.
|
||||
final CkSurface shrunk = surface.acquireRenderSurface(ui.Size(5, 15));
|
||||
final CkSurface shrunk = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
|
||||
expect(shrunk, same(original));
|
||||
|
||||
// The first increase will allocate a new surface, but will overallocate
|
||||
// by 40% to accommodate future increases.
|
||||
final CkSurface firstIncrease = surface.acquireRenderSurface(ui.Size(10, 20));
|
||||
final CkSurface firstIncrease = surface.acquireFrame(ui.Size(10, 20)).skiaSurface;
|
||||
expect(firstIncrease, isNot(same(original)));
|
||||
|
||||
// Expect overallocated dimensions
|
||||
@ -40,11 +42,11 @@ void testMain() {
|
||||
expect(firstIncrease.height(), 28);
|
||||
|
||||
// Subsequent increases within 40% reuse the old surface.
|
||||
final CkSurface secondIncrease = surface.acquireRenderSurface(ui.Size(11, 22));
|
||||
final CkSurface secondIncrease = surface.acquireFrame(ui.Size(11, 22)).skiaSurface;
|
||||
expect(secondIncrease, same(firstIncrease));
|
||||
|
||||
// Increases beyond the 40% limit will cause a new allocation.
|
||||
final CkSurface huge = surface.acquireRenderSurface(ui.Size(20, 40));
|
||||
final CkSurface huge = surface.acquireFrame(ui.Size(20, 40)).skiaSurface;
|
||||
expect(huge, isNot(same(firstIncrease)));
|
||||
|
||||
// Also over-allocated
|
||||
@ -52,8 +54,39 @@ void testMain() {
|
||||
expect(huge.height(), 56);
|
||||
|
||||
// Shrink again. Reuse the last allocated surface.
|
||||
final CkSurface shrunk2 = surface.acquireRenderSurface(ui.Size(5, 15));
|
||||
final CkSurface shrunk2 = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
|
||||
expect(shrunk2, same(huge));
|
||||
});
|
||||
|
||||
test(
|
||||
'Surface creates new context when WebGL context is lost',
|
||||
() async {
|
||||
final Surface surface = Surface(HtmlViewEmbedder());
|
||||
expect(surface.debugForceNewContext, isTrue);
|
||||
final CkSurface before = surface.acquireFrame(ui.Size(9, 19)).skiaSurface;
|
||||
expect(surface.debugForceNewContext, isFalse);
|
||||
|
||||
// Pump a timer to flush any microtasks.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final CkSurface afterAcquireFrame = surface.acquireFrame(ui.Size(9, 19)).skiaSurface;
|
||||
// Existing context is reused.
|
||||
expect(afterAcquireFrame, same(before));
|
||||
|
||||
// Emulate WebGL context loss.
|
||||
final html.CanvasElement canvas = surface.htmlElement.children.single as html.CanvasElement;
|
||||
final dynamic ctx = canvas.getContext('webgl2');
|
||||
final dynamic loseContextExtension = ctx.getExtension('WEBGL_lose_context');
|
||||
loseContextExtension.loseContext();
|
||||
|
||||
// Pump a timer to allow the "lose context" event to propagate.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(surface.debugForceNewContext, isTrue);
|
||||
final CkSurface afterContextLost = surface.acquireFrame(ui.Size(9, 19)).skiaSurface;
|
||||
// A new cotext is created.
|
||||
expect(afterContextLost, isNot(same(before)));
|
||||
},
|
||||
// Firefox doesn't have the WEBGL_lose_context extension.
|
||||
skip: isFirefox || isIosSafari,
|
||||
);
|
||||
}, skip: isIosSafari);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user