| // 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_util' as js_util; |
| |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| import 'package:ui/ui.dart' as ui; |
| |
| import 'common.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() { |
| group('CanvasKit', () { |
| setUpCanvasKitTest(); |
| setUp(() { |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0); |
| }); |
| |
| test('Surface allocates canvases efficiently', () { |
| final Surface surface = Surface(); |
| final CkSurface originalSurface = |
| 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(originalSurface.width(), 9); |
| expect(originalSurface.height(), 19); |
| |
| // Shrinking reuses the existing canvas but translates it so |
| // Skia renders into the visible area. |
| final CkSurface shrunkSurface = |
| surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; |
| final DomOffscreenCanvas shrunk = surface.debugOffscreenCanvas!; |
| expect(shrunk, same(original)); |
| expect(shrunkSurface, isNot(same(originalSurface))); |
| expect(shrunkSurface.width(), 5); |
| expect(shrunkSurface.height(), 15); |
| |
| // The first increase will allocate a new surface, but will overallocate |
| // by 40% to accommodate future increases. |
| final CkSurface firstIncreaseSurface = |
| surface.acquireFrame(const ui.Size(10, 20)).skiaSurface; |
| 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(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 DomOffscreenCanvas secondIncrease = surface.debugOffscreenCanvas!; |
| expect(secondIncrease, same(firstIncrease)); |
| 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 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(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 DomOffscreenCanvas shrunk2 = surface.debugOffscreenCanvas!; |
| expect(shrunk2, same(huge)); |
| expect(shrunkSurface2, isNot(same(hugeSurface))); |
| expect(shrunkSurface2.width(), 5); |
| expect(shrunkSurface2.height(), 15); |
| |
| // Doubling the DPR should halve the CSS width, height, and translation of the canvas. |
| // This tests https://github.com/flutter/flutter/issues/77084 |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); |
| final CkSurface dpr2Surface2 = |
| surface.acquireFrame(const ui.Size(5, 15)).skiaSurface; |
| final DomOffscreenCanvas dpr2Canvas = surface.debugOffscreenCanvas!; |
| expect(dpr2Canvas, same(huge)); |
| expect(dpr2Surface2, isNot(same(hugeSurface))); |
| expect(dpr2Surface2.width(), 5); |
| expect(dpr2Surface2.height(), 15); |
| |
| // Skipping on Firefox for now since Firefox headless doesn't support WebGL |
| // This causes issues in the test since we create a Canvas-backed surface, |
| // which cannot be a different size from the canvas. |
| // TODO(hterkelsen): See if we can give a custom size for software |
| // surfaces. |
| }, skip: isFirefox || !Surface.offscreenCanvasSupported); |
| |
| test('Surface used as DisplayCanvas resizes correctly', () { |
| final Surface surface = Surface(isDisplayCanvas: true); |
| |
| surface.createOrUpdateSurface(const ui.Size(9, 19)); |
| final DomCanvasElement original = getDisplayCanvas(surface); |
| ui.Size canvasSize = getCssSize(surface); |
| |
| // Expect exact requested dimensions. |
| expect(original.width, 9); |
| expect(original.height, 19); |
| expect(canvasSize.width, 9); |
| expect(canvasSize.height, 19); |
| |
| // Shrinking reuses the existing canvas but translates it so |
| // Skia renders into the visible area. |
| surface.createOrUpdateSurface(const ui.Size(5, 15)); |
| final DomCanvasElement shrunk = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| expect(shrunk.width, 9); |
| expect(shrunk.height, 19); |
| expect(canvasSize.width, 9); |
| expect(canvasSize.height, 19); |
| |
| // The first increase will allocate a new surface, but will overallocate |
| // by 40% to accommodate future increases. |
| surface.createOrUpdateSurface(const ui.Size(10, 20)); |
| final DomCanvasElement firstIncrease = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| |
| expect(firstIncrease, same(original)); |
| |
| // Expect overallocated dimensions |
| expect(firstIncrease.width, 14); |
| expect(firstIncrease.height, 28); |
| expect(canvasSize.width, 14); |
| expect(canvasSize.height, 28); |
| |
| // Subsequent increases within 40% reuse the old canvas. |
| surface.createOrUpdateSurface(const ui.Size(11, 22)); |
| final DomCanvasElement secondIncrease = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| |
| expect(secondIncrease, same(firstIncrease)); |
| expect(secondIncrease.width, 14); |
| expect(secondIncrease.height, 28); |
| expect(canvasSize.width, 14); |
| expect(canvasSize.height, 28); |
| |
| // Increases beyond the 40% limit will cause a new allocation. |
| surface.createOrUpdateSurface(const ui.Size(20, 40)); |
| final DomCanvasElement huge = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| |
| expect(huge, same(secondIncrease)); |
| |
| // Also over-allocated |
| expect(huge.width, 28); |
| expect(huge.height, 56); |
| expect(canvasSize.width, 28); |
| expect(canvasSize.height, 56); |
| |
| // Shrink again. Reuse the last allocated surface. |
| surface.createOrUpdateSurface(const ui.Size(5, 15)); |
| final DomCanvasElement shrunk2 = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| |
| expect(shrunk2, same(huge)); |
| expect(shrunk2.width, 28); |
| expect(shrunk2.height, 56); |
| expect(canvasSize.width, 28); |
| expect(canvasSize.height, 56); |
| |
| // Doubling the DPR should halve the CSS width, height, and translation of the canvas. |
| // This tests https://github.com/flutter/flutter/issues/77084 |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); |
| surface.createOrUpdateSurface(const ui.Size(5, 15)); |
| final DomCanvasElement dpr2Canvas = getDisplayCanvas(surface); |
| canvasSize = getCssSize(surface); |
| |
| expect(dpr2Canvas, same(huge)); |
| expect(dpr2Canvas.width, 28); |
| expect(dpr2Canvas.height, 56); |
| // Canvas is half the size in logical pixels because device pixel ratio is |
| // 2.0. |
| expect(canvasSize.width, 14); |
| expect(canvasSize.height, 28); |
| // Skip on wasm since same() doesn't work for JSValues. |
| }, skip: isWasm); |
| |
| test( |
| 'Surface creates new context when WebGL context is restored', |
| () async { |
| final Surface surface = Surface(); |
| expect(surface.debugForceNewContext, isTrue); |
| final CkSurface before = |
| surface.acquireFrame(const 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(const ui.Size(9, 19)).skiaSurface; |
| // Existing context is reused. |
| expect(afterAcquireFrame, same(before)); |
| |
| // Emulate WebGL context loss. |
| final DomOffscreenCanvas canvas = surface.debugOffscreenCanvas!; |
| final Object ctx = canvas.getContext('webgl2')!; |
| final Object loseContextExtension = js_util.callMethod( |
| ctx, |
| 'getExtension', |
| <String>['WEBGL_lose_context'], |
| ); |
| js_util.callMethod<void>( |
| loseContextExtension, 'loseContext', const <void>[]); |
| |
| // Pump a timer to allow the "lose context" event to propagate. |
| await Future<void>.delayed(Duration.zero); |
| // We don't create a new GL context until the context is restored. |
| expect(surface.debugContextLost, isTrue); |
| final bool isContextLost = |
| js_util.callMethod<bool>(ctx, 'isContextLost', const <void>[]); |
| expect(isContextLost, isTrue); |
| |
| // Emulate WebGL context restoration. |
| js_util.callMethod<void>( |
| loseContextExtension, 'restoreContext', const <void>[]); |
| |
| // Pump a timer to allow the "restore context" event to propagate. |
| await Future<void>.delayed(Duration.zero); |
| expect(surface.debugForceNewContext, isTrue); |
| |
| final CkSurface afterContextLost = |
| surface.acquireFrame(const ui.Size(9, 19)).skiaSurface; |
| // A new context is created. |
| expect(afterContextLost, isNot(same(before))); |
| }, |
| // Firefox can't create a WebGL2 context in headless mode. |
| skip: isFirefox || !Surface.offscreenCanvasSupported, |
| ); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/75286 |
| test( |
| 'updates canvas logical size when device-pixel ratio changes', |
| () { |
| final Surface surface = Surface(); |
| final CkSurface original = |
| surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; |
| |
| expect(original.width(), 10); |
| expect(original.height(), 16); |
| 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. |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); |
| final CkSurface highDpr = |
| surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; |
| expect(highDpr.width(), 10); |
| expect(highDpr.height(), 16); |
| 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. |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(0.5); |
| final CkSurface lowDpr = |
| surface.acquireFrame(const ui.Size(10, 16)).skiaSurface; |
| expect(lowDpr.width(), 10); |
| expect(lowDpr.height(), 16); |
| expect(surface.debugOffscreenCanvas!.width, 10); |
| expect(surface.debugOffscreenCanvas!.height, 16); |
| |
| // See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172 |
| EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0); |
| final CkSurface changeRatioAndSize = |
| surface.acquireFrame(const ui.Size(9.9, 15.9)).skiaSurface; |
| expect(changeRatioAndSize.width(), 10); |
| expect(changeRatioAndSize.height(), 16); |
| expect(surface.debugOffscreenCanvas!.width, 10); |
| expect(surface.debugOffscreenCanvas!.height, 16); |
| }, |
| skip: !Surface.offscreenCanvasSupported, |
| ); |
| }); |
| } |
| |
| DomCanvasElement getDisplayCanvas(Surface surface) { |
| assert(surface.isDisplayCanvas); |
| return surface.hostElement.children.first as DomCanvasElement; |
| } |
| |
| /// Extracts the CSS style values of 'width' and 'height' and returns them |
| /// as a [ui.Size]. |
| ui.Size getCssSize(Surface surface) { |
| final DomCanvasElement canvas = getDisplayCanvas(surface); |
| final String cssWidth = canvas.style.width; |
| final String cssHeight = canvas.style.height; |
| // CSS width and height should be in the form 'NNNpx'. So cut off the 'px' and |
| // convert to a number. |
| final double width = |
| double.parse(cssWidth.substring(0, cssWidth.length - 2).trim()); |
| final double height = |
| double.parse(cssHeight.substring(0, cssHeight.length - 2).trim()); |
| return ui.Size(width, height); |
| } |