mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Revert "[reland] Unify canvas creation and Surface code in Skwasm and CanvasK…"
This reverts commit effb2e1ac169524ffc21b72dde571a04d6c0d898.
This commit is contained in:
parent
8491d3149d
commit
a442e2c50f
@ -28,7 +28,9 @@ export 'engine/canvaskit/image_filter.dart';
|
||||
export 'engine/canvaskit/image_wasm_codecs.dart';
|
||||
export 'engine/canvaskit/image_web_codecs.dart';
|
||||
export 'engine/canvaskit/mask_filter.dart';
|
||||
export 'engine/canvaskit/multi_surface_rasterizer.dart';
|
||||
export 'engine/canvaskit/native_memory.dart';
|
||||
export 'engine/canvaskit/offscreen_canvas_rasterizer.dart';
|
||||
export 'engine/canvaskit/painting.dart';
|
||||
export 'engine/canvaskit/path.dart';
|
||||
export 'engine/canvaskit/path_metrics.dart';
|
||||
@ -42,14 +44,10 @@ export 'engine/canvaskit/util.dart';
|
||||
export 'engine/canvaskit/vertices.dart';
|
||||
export 'engine/clipboard.dart';
|
||||
export 'engine/color_filter.dart';
|
||||
export 'engine/compositing/canvas_provider.dart';
|
||||
export 'engine/compositing/composition.dart';
|
||||
export 'engine/compositing/display_canvas_factory.dart';
|
||||
export 'engine/compositing/multi_surface_rasterizer.dart';
|
||||
export 'engine/compositing/offscreen_canvas_rasterizer.dart';
|
||||
export 'engine/compositing/rasterizer.dart';
|
||||
export 'engine/compositing/render_canvas.dart';
|
||||
export 'engine/compositing/surface.dart';
|
||||
export 'engine/configuration.dart';
|
||||
export 'engine/display.dart';
|
||||
export 'engine/dom.dart';
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
// 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 display Skia-drawn content.
|
||||
class DisplayCanvasFactory<T extends DisplayCanvas> {
|
||||
DisplayCanvasFactory({required this.createCanvas}) {
|
||||
assert(() {
|
||||
registerHotRestartListener(dispose);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
/// A function which is passed in as a constructor parameter which is used to
|
||||
/// create new display canvases.
|
||||
final T Function() createCanvas;
|
||||
|
||||
/// 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.
|
||||
late final T baseCanvas = createCanvas()..initialize();
|
||||
|
||||
/// Canvases created by this factory which are currently in use.
|
||||
final List<T> _liveCanvases = <T>[];
|
||||
|
||||
/// Canvases created by this factory which are no longer in use. These can be
|
||||
/// reused.
|
||||
final List<T> _cache = <T>[];
|
||||
|
||||
/// 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 a display canvas from the cache or creates a new one if there are
|
||||
/// none in the cache.
|
||||
T getCanvas() {
|
||||
if (_cache.isNotEmpty) {
|
||||
final T canvas = _cache.removeLast();
|
||||
_liveCanvases.add(canvas);
|
||||
return canvas;
|
||||
} else {
|
||||
final T canvas = createCanvas();
|
||||
canvas.initialize();
|
||||
_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 canvases except the base canvas from the DOM.
|
||||
///
|
||||
/// This is called at the beginning of the frame to prepare for painting into
|
||||
/// the new canvases.
|
||||
void removeCanvasesFromDom() {
|
||||
_cache.forEach(_removeFromDom);
|
||||
_liveCanvases.forEach(_removeFromDom);
|
||||
}
|
||||
|
||||
/// Calls [callback] on each canvas created by this factory.
|
||||
void forEachCanvas(void Function(T canvas) callback) {
|
||||
callback(baseCanvas);
|
||||
_cache.forEach(callback);
|
||||
_liveCanvases.forEach(callback);
|
||||
}
|
||||
|
||||
// Removes [canvas] from the DOM.
|
||||
void _removeFromDom(T canvas) {
|
||||
canvas.hostElement.remove();
|
||||
}
|
||||
|
||||
/// Signals that a canvas is no longer being used. It can be reused.
|
||||
void releaseCanvas(T 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.hostElement.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(T canvas) {
|
||||
if (canvas == baseCanvas || _liveCanvases.contains(canvas)) {
|
||||
return true;
|
||||
}
|
||||
assert(_cache.contains(canvas));
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Dispose all canvases created by this factory.
|
||||
void dispose() {
|
||||
for (final T canvas in _cache) {
|
||||
canvas.dispose();
|
||||
}
|
||||
for (final T canvas in _liveCanvases) {
|
||||
canvas.dispose();
|
||||
}
|
||||
baseCanvas.dispose();
|
||||
_liveCanvases.clear();
|
||||
_cache.clear();
|
||||
}
|
||||
}
|
||||
@ -557,15 +557,11 @@ class CkImage implements ui.Image, StackTraceDebugger {
|
||||
|
||||
ByteData? _readPixelsFromImageViaSurface(ui.ImageByteFormat format) {
|
||||
final Surface surface = CanvasKitRenderer.instance.pictureToImageSurface;
|
||||
surface.setSize(BitmapSize(width, height));
|
||||
final ckSurface = surface as CkSurface;
|
||||
final SkSurface skiaSurface = ckSurface.skSurface!;
|
||||
|
||||
final ckCanvas = CkCanvas.fromSkCanvas(skiaSurface.getCanvas());
|
||||
final CkSurface ckSurface = surface.createOrUpdateSurface(BitmapSize(width, height));
|
||||
final CkCanvas ckCanvas = ckSurface.getCanvas();
|
||||
ckCanvas.clear(const ui.Color(0x00000000));
|
||||
ckCanvas.drawImage(this, ui.Offset.zero, CkPaint());
|
||||
final SkImage skImage = skiaSurface.makeImageSnapshot();
|
||||
|
||||
final SkImage skImage = ckSurface.surface.makeImageSnapshot();
|
||||
final imageInfo = SkImageInfo(
|
||||
alphaType: canvasKit.AlphaType.Premul,
|
||||
colorType: canvasKit.ColorType.RGBA_8888,
|
||||
@ -574,8 +570,6 @@ class CkImage implements ui.Image, StackTraceDebugger {
|
||||
height: height.toDouble(),
|
||||
);
|
||||
final Uint8List? pixels = skImage.readPixels(0, 0, imageInfo);
|
||||
skImage.delete();
|
||||
|
||||
if (pixels == null) {
|
||||
throw StateError('Unable to convert read pixels from SkImage.');
|
||||
}
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
// 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:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
/// A Rasterizer which uses one or many on-screen WebGL contexts to display the
|
||||
/// scene. This way of rendering is prone to bugs because there is a limit to
|
||||
/// how many WebGL contexts can be live at one time as well as bugs in sharing
|
||||
/// GL resources between the contexts. However, using [createImageBitmap] is
|
||||
/// currently very slow on Firefox and Safari browsers, so directly rendering
|
||||
/// to several [Surface]s is how we can achieve 60 fps on these browsers.
|
||||
class MultiSurfaceRasterizer extends Rasterizer {
|
||||
@override
|
||||
MultiSurfaceViewRasterizer createViewRasterizer(EngineFlutterView view) {
|
||||
return _viewRasterizers.putIfAbsent(view, () => MultiSurfaceViewRasterizer(view, this));
|
||||
}
|
||||
|
||||
final Map<EngineFlutterView, MultiSurfaceViewRasterizer> _viewRasterizers =
|
||||
<EngineFlutterView, MultiSurfaceViewRasterizer>{};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final MultiSurfaceViewRasterizer viewRasterizer in _viewRasterizers.values) {
|
||||
viewRasterizer.dispose();
|
||||
}
|
||||
_viewRasterizers.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void setResourceCacheMaxBytes(int bytes) {
|
||||
for (final MultiSurfaceViewRasterizer viewRasterizer in _viewRasterizers.values) {
|
||||
viewRasterizer.displayFactory.forEachCanvas((Surface surface) {
|
||||
surface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSurfaceViewRasterizer extends ViewRasterizer {
|
||||
MultiSurfaceViewRasterizer(super.view, this.rasterizer);
|
||||
|
||||
final MultiSurfaceRasterizer rasterizer;
|
||||
|
||||
@override
|
||||
final DisplayCanvasFactory<Surface> displayFactory = DisplayCanvasFactory<Surface>(
|
||||
createCanvas: () => Surface(isDisplayCanvas: true),
|
||||
);
|
||||
|
||||
@override
|
||||
void prepareToDraw() {
|
||||
displayFactory.baseCanvas.createOrUpdateSurface(currentFrameSize);
|
||||
}
|
||||
|
||||
Future<void> rasterizeToCanvas(DisplayCanvas canvas, ui.Picture picture) {
|
||||
final surface = canvas as Surface;
|
||||
surface.createOrUpdateSurface(currentFrameSize);
|
||||
surface.positionToShowFrame(currentFrameSize);
|
||||
final CkCanvas skCanvas = surface.getCanvas();
|
||||
skCanvas.clear(const ui.Color(0x00000000));
|
||||
skCanvas.drawPicture(picture);
|
||||
surface.flush();
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rasterize(
|
||||
List<DisplayCanvas> displayCanvases,
|
||||
List<ui.Picture> pictures,
|
||||
FrameTimingRecorder? recorder,
|
||||
) async {
|
||||
if (displayCanvases.length != pictures.length) {
|
||||
throw ArgumentError('Called rasterize() with a different number of canvases and pictures.');
|
||||
}
|
||||
final rasterizeFutures = <Future<void>>[];
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
rasterizeFutures.add(rasterizeToCanvas(displayCanvases[i], pictures[i]));
|
||||
}
|
||||
recorder?.recordRasterStart();
|
||||
await Future.wait<void>(rasterizeFutures);
|
||||
recorder?.recordRasterFinish();
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
// 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 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
@ -10,22 +9,9 @@ import 'package:ui/ui.dart' as ui;
|
||||
/// all the rendering. It transfers bitmaps created in the OffscreenCanvas to
|
||||
/// one or many on-screen <canvas> elements to actually display the scene.
|
||||
class OffscreenCanvasRasterizer extends Rasterizer {
|
||||
OffscreenCanvasRasterizer(
|
||||
OffscreenSurface Function(OffscreenCanvasProvider) offscreenSurfaceCreateFn,
|
||||
) : _surfaceProvider = OffscreenSurfaceProvider(
|
||||
OffscreenCanvasProvider(),
|
||||
offscreenSurfaceCreateFn,
|
||||
);
|
||||
|
||||
final OffscreenSurfaceProvider _surfaceProvider;
|
||||
|
||||
@override
|
||||
@visibleForTesting
|
||||
SurfaceProvider get surfaceProvider => _surfaceProvider;
|
||||
|
||||
/// This is an SkSurface backed by an OffScreenCanvas. This single Surface is
|
||||
/// used to render to many RenderCanvases to produce the rendered scene.
|
||||
late final OffscreenSurface offscreenSurface = _surfaceProvider.createSurface();
|
||||
final Surface offscreenSurface = Surface();
|
||||
|
||||
@override
|
||||
OffscreenCanvasViewRasterizer createViewRasterizer(EngineFlutterView view) {
|
||||
@ -37,21 +23,16 @@ class OffscreenCanvasRasterizer extends Rasterizer {
|
||||
|
||||
@override
|
||||
void setResourceCacheMaxBytes(int bytes) {
|
||||
_surfaceProvider.setSkiaResourceCacheMaxBytes(bytes);
|
||||
offscreenSurface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_surfaceProvider.dispose();
|
||||
offscreenSurface.dispose();
|
||||
for (final OffscreenCanvasViewRasterizer viewRasterizer in _viewRasterizers.values) {
|
||||
viewRasterizer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Surface createPictureToImageSurface() {
|
||||
return _surfaceProvider.createSurface();
|
||||
}
|
||||
}
|
||||
|
||||
class OffscreenCanvasViewRasterizer extends ViewRasterizer {
|
||||
@ -64,9 +45,18 @@ class OffscreenCanvasViewRasterizer extends ViewRasterizer {
|
||||
createCanvas: () => RenderCanvas(),
|
||||
);
|
||||
|
||||
/// Render the given [picture] so it is displayed by the given [canvas].
|
||||
Future<void> rasterizeToCanvas(DisplayCanvas canvas, ui.Picture picture) async {
|
||||
await rasterizer.offscreenSurface.rasterizeToCanvas(
|
||||
currentFrameSize,
|
||||
canvas as RenderCanvas,
|
||||
picture,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> prepareToDraw() {
|
||||
return rasterizer.offscreenSurface.setSize(currentFrameSize);
|
||||
void prepareToDraw() {
|
||||
rasterizer.offscreenSurface.createOrUpdateSurface(currentFrameSize);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -78,23 +68,12 @@ class OffscreenCanvasViewRasterizer extends ViewRasterizer {
|
||||
if (displayCanvases.length != pictures.length) {
|
||||
throw ArgumentError('Called rasterize() with a different number of canvases and pictures.');
|
||||
}
|
||||
recorder?.recordRasterStart();
|
||||
if (browserSupportsCreateImageBitmap) {
|
||||
final List<DomImageBitmap> bitmaps = await rasterizer.offscreenSurface
|
||||
.rasterizeToImageBitmaps(pictures);
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
(displayCanvases[i] as RenderCanvas).render(bitmaps[i]);
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
await rasterizer.offscreenSurface.rasterizeToCanvas(pictures[i]);
|
||||
(displayCanvases[i] as RenderCanvas).renderWithNoBitmapSupport(
|
||||
rasterizer.offscreenSurface.canvasImageSource,
|
||||
currentFrameSize.height,
|
||||
currentFrameSize,
|
||||
);
|
||||
}
|
||||
final rasterizeFutures = <Future<void>>[];
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
rasterizeFutures.add(rasterizeToCanvas(displayCanvases[i], pictures[i]));
|
||||
}
|
||||
recorder?.recordRasterStart();
|
||||
await Future.wait<void>(rasterizeFutures);
|
||||
recorder?.recordRasterFinish();
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,6 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../compositing/surface.dart';
|
||||
import '../layer/layer_painting.dart';
|
||||
import '../util.dart';
|
||||
import 'canvas.dart';
|
||||
@ -115,18 +114,11 @@ class CkPicture implements LayerPicture, StackTraceDebugger {
|
||||
assert(debugCheckNotDisposed('Cannot convert picture to image.'));
|
||||
|
||||
final Surface surface = CanvasKitRenderer.instance.pictureToImageSurface;
|
||||
surface.setSize(BitmapSize(width, height));
|
||||
final ckSurface = surface as CkSurface;
|
||||
final SkSurface skiaSurface = ckSurface.skSurface!;
|
||||
|
||||
final ckCanvas = CkCanvas.fromSkCanvas(skiaSurface.getCanvas());
|
||||
final CkSurface ckSurface = surface.createOrUpdateSurface(BitmapSize(width, height));
|
||||
final CkCanvas ckCanvas = ckSurface.getCanvas();
|
||||
ckCanvas.clear(const ui.Color(0x00000000));
|
||||
ckCanvas.drawPicture(this);
|
||||
final SkImage skImage = skiaSurface.makeImageSnapshot();
|
||||
|
||||
// TODO(hterkelsen): This is a hack to get the pixels from the SkImage.
|
||||
// We should be able to do this without creating a new image. This is
|
||||
// a workaround for a bug in CanvasKit.
|
||||
final SkImage skImage = ckSurface.surface.makeImageSnapshot();
|
||||
final imageInfo = SkImageInfo(
|
||||
alphaType: canvasKit.AlphaType.Premul,
|
||||
colorType: canvasKit.ColorType.RGBA_8888,
|
||||
@ -135,9 +127,8 @@ class CkPicture implements LayerPicture, StackTraceDebugger {
|
||||
height: height.toDouble(),
|
||||
);
|
||||
final Uint8List? pixels = skImage.readPixels(0, 0, imageInfo);
|
||||
skImage.delete();
|
||||
if (pixels == null) {
|
||||
throw StateError('Unable to convert read pixels from SkImage.');
|
||||
throw StateError('Unable to read pixels from SkImage.');
|
||||
}
|
||||
final SkImage? rasterImage = canvasKit.MakeImage(imageInfo, pixels, (4 * width).toDouble());
|
||||
if (rasterImage == null) {
|
||||
@ -162,7 +153,4 @@ class CkPicture implements LayerPicture, StackTraceDebugger {
|
||||
|
||||
@override
|
||||
StackTrace get debugStackTrace => _debugStackTrace;
|
||||
|
||||
@override
|
||||
bool get isDisposed => _isDisposed;
|
||||
}
|
||||
|
||||
@ -32,21 +32,20 @@ class CanvasKitRenderer extends Renderer {
|
||||
|
||||
static Rasterizer _createRasterizer() {
|
||||
if (configuration.canvasKitForceMultiSurfaceRasterizer || isSafari || isFirefox) {
|
||||
return MultiSurfaceRasterizer(
|
||||
(OnscreenCanvasProvider canvasProvider) => CkOnscreenSurface(canvasProvider),
|
||||
);
|
||||
return MultiSurfaceRasterizer();
|
||||
}
|
||||
return OffscreenCanvasRasterizer(
|
||||
(OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider),
|
||||
);
|
||||
return OffscreenCanvasRasterizer();
|
||||
}
|
||||
|
||||
@override
|
||||
void debugResetRasterizer() {
|
||||
rasterizer = _createRasterizer();
|
||||
_pictureToImageSurface = rasterizer.createPictureToImageSurface();
|
||||
}
|
||||
|
||||
/// A surface used specifically for `Picture.toImage` when software rendering
|
||||
/// is supported.
|
||||
final Surface pictureToImageSurface = Surface();
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
_initialized ??= () async {
|
||||
@ -60,7 +59,6 @@ class CanvasKitRenderer extends Renderer {
|
||||
windowFlutterCanvasKit = canvasKit;
|
||||
}
|
||||
rasterizer = _createRasterizer();
|
||||
_pictureToImageSurface = rasterizer.createPictureToImageSurface();
|
||||
_instance = this;
|
||||
await super.initialize();
|
||||
}();
|
||||
@ -496,9 +494,4 @@ class CanvasKitRenderer extends Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
late Surface _pictureToImageSurface;
|
||||
|
||||
@override
|
||||
Surface get pictureToImageSurface => _pictureToImageSurface;
|
||||
}
|
||||
|
||||
@ -2,338 +2,557 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../browser_detection.dart';
|
||||
import '../compositing/rasterizer.dart';
|
||||
import '../compositing/render_canvas.dart';
|
||||
import '../configuration.dart';
|
||||
import '../display.dart';
|
||||
import '../dom.dart';
|
||||
import '../platform_dispatcher.dart';
|
||||
import '../util.dart';
|
||||
import 'canvas.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'util.dart';
|
||||
|
||||
// Only supported in profile/release mode. Allows Flutter to use MSAA but
|
||||
// removes the ability for disabling AA on Paint objects.
|
||||
const bool _kUsingMSAA = bool.fromEnvironment('flutter.canvaskit.msaa');
|
||||
|
||||
/// The base class for CanvasKit surfaces, containing shared logic for context
|
||||
/// management and Skia object creation.
|
||||
abstract class CkSurface extends Surface {
|
||||
CkSurface(this._canvasProvider) {
|
||||
_canvas = _canvasProvider.acquireCanvas(_currentSize, onContextLost: onContextLost);
|
||||
_maybeAttachCanvasToDom();
|
||||
_initialize();
|
||||
/// A surface which can be drawn into by the compositor.
|
||||
///
|
||||
/// The underlying representation is a [CkSurface], which can be reused by
|
||||
/// successive frames if they are the same size. Otherwise, a new [CkSurface] is
|
||||
/// created.
|
||||
class Surface extends DisplayCanvas {
|
||||
Surface({this.isDisplayCanvas = false})
|
||||
: useOffscreenCanvas = Surface.offscreenCanvasSupported && !isDisplayCanvas;
|
||||
|
||||
CkSurface? _surface;
|
||||
|
||||
/// Returns the underlying CanvasKit Surface. Should only be used in tests.
|
||||
CkSurface? debugGetCkSurface() {
|
||||
var assertsEnabled = false;
|
||||
assert(() {
|
||||
assertsEnabled = true;
|
||||
return true;
|
||||
}());
|
||||
if (!assertsEnabled) {
|
||||
throw StateError('debugGetCkSurface() can only be used in tests');
|
||||
}
|
||||
return _surface;
|
||||
}
|
||||
|
||||
final CanvasProvider _canvasProvider;
|
||||
/// Whether or not to use an `OffscreenCanvas` to back this [Surface].
|
||||
final bool useOffscreenCanvas;
|
||||
|
||||
BitmapSize _currentSize = const BitmapSize(1, 1);
|
||||
/// If `true`, this [Surface] is used as a [DisplayCanvas].
|
||||
final bool isDisplayCanvas;
|
||||
|
||||
/// The underlying Skia surface object.
|
||||
SkSurface? get skSurface => _skSurface;
|
||||
SkSurface? _skSurface;
|
||||
/// 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;
|
||||
|
||||
/// Whether or not WebGl is supported.
|
||||
bool _contextLost = false;
|
||||
bool get debugContextLost => _contextLost;
|
||||
|
||||
/// Forces AssertionError when attempting to create a CPU-based surface.
|
||||
/// Only for tests.
|
||||
bool debugThrowOnSoftwareSurfaceCreation = false;
|
||||
|
||||
/// A cached copy of the most recently created `webglcontextlost` listener.
|
||||
///
|
||||
/// This defaults to true unless `canvasKitForceCpuOnly` is set to true or
|
||||
/// `webGLVersion` is -1. If Skia fails to create a GrContext, this will be
|
||||
/// set to false.
|
||||
@visibleForTesting
|
||||
bool get supportsWebGl {
|
||||
if (configuration.canvasKitForceCpuOnly) {
|
||||
_fallbackToSoftwareReason = 'canvasKitForceCpuOnly is set to true';
|
||||
return false;
|
||||
/// We must cache this function because each time we access the tear-off it
|
||||
/// creates a new object, meaning we won't be able to remove this listener
|
||||
/// later.
|
||||
DomEventListener? _cachedContextLostListener;
|
||||
|
||||
/// A cached copy of the most recently created `webglcontextrestored`
|
||||
/// listener.
|
||||
///
|
||||
/// We must cache this function because each time we access the tear-off it
|
||||
/// creates a new object, meaning we won't be able to remove this listener
|
||||
/// later.
|
||||
DomEventListener? _cachedContextRestoredListener;
|
||||
|
||||
SkGrContext? _grContext;
|
||||
int? _glContext;
|
||||
int? _skiaCacheBytes;
|
||||
|
||||
/// The underlying OffscreenCanvas element used for this surface.
|
||||
DomOffscreenCanvas? _offscreenCanvas;
|
||||
|
||||
/// Returns the underlying OffscreenCanvas. Should only be used in tests.
|
||||
DomOffscreenCanvas? debugGetOffscreenCanvas() {
|
||||
var assertsEnabled = false;
|
||||
assert(() {
|
||||
assertsEnabled = true;
|
||||
return true;
|
||||
}());
|
||||
if (!assertsEnabled) {
|
||||
throw StateError('debugGetOffscreenCanvas() can only be used in tests');
|
||||
}
|
||||
if (webGLVersion == -1) {
|
||||
_fallbackToSoftwareReason = 'webGLVersion is -1';
|
||||
return false;
|
||||
}
|
||||
if (_failedToCreateGrContext) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return _offscreenCanvas;
|
||||
}
|
||||
|
||||
String? _fallbackToSoftwareReason;
|
||||
/// The <canvas> backing this Surface in the case that OffscreenCanvas isn't
|
||||
/// supported.
|
||||
DomHTMLCanvasElement? _canvasElement;
|
||||
|
||||
/// When true, the surface will fail to create a GL context and fall back to
|
||||
/// software rendering. This is useful for testing.
|
||||
@visibleForTesting
|
||||
static bool debugForceGLFailure = false;
|
||||
/// Note, if this getter is called, then this Surface is being used as an
|
||||
/// overlay and must be backed by an onscreen <canvas> element.
|
||||
@override
|
||||
final DomElement hostElement = createDomElement('flt-canvas-container');
|
||||
|
||||
bool _failedToCreateGrContext = false;
|
||||
int _pixelWidth = -1;
|
||||
int _pixelHeight = -1;
|
||||
double _currentDevicePixelRatio = -1;
|
||||
int _sampleCount = -1;
|
||||
int _stencilBits = -1;
|
||||
|
||||
/// Specify the GPU resource cache limits.
|
||||
void setSkiaResourceCacheMaxBytes(int bytes) {
|
||||
_skiaCacheBytes = bytes;
|
||||
_syncCacheBytes();
|
||||
}
|
||||
|
||||
void _syncCacheBytes() {
|
||||
if (_skiaCacheBytes != null) {
|
||||
_grContext?.setResourceCacheLimitBytes(_skiaCacheBytes!.toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
/// The CanvasKit canvas associated with this surface.
|
||||
CkCanvas getCanvas() {
|
||||
return _surface!.getCanvas();
|
||||
}
|
||||
|
||||
void flush() {
|
||||
_surface!.flush();
|
||||
}
|
||||
|
||||
Future<void> rasterizeToCanvas(
|
||||
BitmapSize bitmapSize,
|
||||
RenderCanvas canvas,
|
||||
ui.Picture picture,
|
||||
) async {
|
||||
final CkCanvas skCanvas = getCanvas();
|
||||
skCanvas.clear(const ui.Color(0x00000000));
|
||||
skCanvas.drawPicture(picture);
|
||||
flush();
|
||||
|
||||
if (browserSupportsCreateImageBitmap) {
|
||||
JSObject bitmapSource;
|
||||
DomImageBitmap bitmap;
|
||||
if (useOffscreenCanvas) {
|
||||
bitmap = _offscreenCanvas!.transferToImageBitmap();
|
||||
} else {
|
||||
bitmapSource = _canvasElement!;
|
||||
bitmap = await createImageBitmap(bitmapSource, (
|
||||
x: 0,
|
||||
y: _pixelHeight - bitmapSize.height,
|
||||
width: bitmapSize.width,
|
||||
height: bitmapSize.height,
|
||||
));
|
||||
}
|
||||
canvas.render(bitmap);
|
||||
} else {
|
||||
// If the browser doesn't support `createImageBitmap` (e.g. Safari 14)
|
||||
// then render using `drawImage` instead.
|
||||
DomCanvasImageSource imageSource;
|
||||
if (useOffscreenCanvas) {
|
||||
imageSource = _offscreenCanvas! as DomCanvasImageSource;
|
||||
} else {
|
||||
imageSource = _canvasElement! as DomCanvasImageSource;
|
||||
}
|
||||
canvas.renderWithNoBitmapSupport(imageSource, _pixelHeight, bitmapSize);
|
||||
}
|
||||
}
|
||||
|
||||
BitmapSize? _currentCanvasPhysicalSize;
|
||||
|
||||
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
|
||||
/// pixels.
|
||||
void _updateLogicalHtmlCanvasSize() {
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
final double logicalWidth = _pixelWidth / devicePixelRatio;
|
||||
final double logicalHeight = _pixelHeight / devicePixelRatio;
|
||||
final DomCSSStyleDeclaration style = _canvasElement!.style;
|
||||
style.width = '${logicalWidth}px';
|
||||
style.height = '${logicalHeight}px';
|
||||
_currentDevicePixelRatio = devicePixelRatio;
|
||||
}
|
||||
|
||||
/// The <canvas> element backing this surface may be larger than the screen.
|
||||
/// The Surface will draw the frame to the bottom left of the <canvas>, but
|
||||
/// the <canvas> is, by default, positioned so that the top left corner is in
|
||||
/// the top left of the window. We need to shift the canvas down so that the
|
||||
/// bottom left of the <canvas> is the the bottom left corner of the window.
|
||||
void positionToShowFrame(BitmapSize frameSize) {
|
||||
assert(isDisplayCanvas, 'Should not position Surface if not used as a render canvas');
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
final double logicalHeight = _pixelHeight / devicePixelRatio;
|
||||
final double logicalFrameHeight = frameSize.height / devicePixelRatio;
|
||||
|
||||
// Shift the canvas up so the bottom left is in the window.
|
||||
_canvasElement!.style.transform = 'translate(0px, ${logicalFrameHeight - logicalHeight}px)';
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Ensure that the initial surface exists and has a size of at least [size].
|
||||
///
|
||||
/// If not provided, [size] defaults to 1x1.
|
||||
///
|
||||
/// This also ensures that the gl/grcontext have been populated so
|
||||
/// that software rendering can be detected.
|
||||
void ensureSurface([BitmapSize size = const BitmapSize(1, 1)]) {
|
||||
// If the GrContext hasn't been setup yet then we need to force initialization
|
||||
// of the canvas and initial surface.
|
||||
if (_surface != null) {
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
// This is the first frame we have rendered with this canvas.
|
||||
createOrUpdateSurface(size);
|
||||
}
|
||||
|
||||
/// Creates a <canvas> and SkSurface for the given [size].
|
||||
CkSurface createOrUpdateSurface(BitmapSize size) {
|
||||
if (size.isEmpty) {
|
||||
throw CanvasKitError('Cannot create surfaces of empty size.');
|
||||
}
|
||||
|
||||
if (!_forceNewContext) {
|
||||
// Check if the window is the same size as before, and if so, don't allocate
|
||||
// a new canvas as the previous canvas is big enough to fit everything.
|
||||
final BitmapSize? previousSurfaceSize = _surface?._size;
|
||||
if (previousSurfaceSize != null &&
|
||||
size.width == previousSurfaceSize.width &&
|
||||
size.height == previousSurfaceSize.height) {
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
if (isDisplayCanvas && devicePixelRatio != _currentDevicePixelRatio) {
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
return _surface!;
|
||||
}
|
||||
|
||||
if (_currentCanvasPhysicalSize != null &&
|
||||
(size.width != _currentCanvasPhysicalSize!.width ||
|
||||
size.height != _currentCanvasPhysicalSize!.height)) {
|
||||
_surface?.dispose();
|
||||
_surface = null;
|
||||
_pixelWidth = size.width;
|
||||
_pixelHeight = size.height;
|
||||
if (useOffscreenCanvas) {
|
||||
_offscreenCanvas!.width = _pixelWidth.toDouble();
|
||||
_offscreenCanvas!.height = _pixelHeight.toDouble();
|
||||
} else {
|
||||
_canvasElement!.width = _pixelWidth.toDouble();
|
||||
_canvasElement!.height = _pixelHeight.toDouble();
|
||||
}
|
||||
_currentCanvasPhysicalSize = BitmapSize(_pixelWidth, _pixelHeight);
|
||||
if (isDisplayCanvas) {
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reached here, then this is the first frame and we haven't made a
|
||||
// surface yet, we are forcing a new context, or the size of the surface
|
||||
// has changed and we need to make a new one.
|
||||
_surface?.dispose();
|
||||
_surface = null;
|
||||
|
||||
// Either a new context is being forced or we've never had one.
|
||||
if (_forceNewContext || _currentCanvasPhysicalSize == null) {
|
||||
_grContext?.releaseResourcesAndAbandonContext();
|
||||
_grContext?.delete();
|
||||
_grContext = null;
|
||||
|
||||
_createNewCanvas(size);
|
||||
_currentCanvasPhysicalSize = size;
|
||||
}
|
||||
|
||||
return _surface = _createNewSurface(size);
|
||||
}
|
||||
|
||||
void _contextRestoredListener(DomEvent event) {
|
||||
assert(
|
||||
_contextLost,
|
||||
'Received "webglcontextrestored" event but never received '
|
||||
'a "webglcontextlost" event.',
|
||||
);
|
||||
_contextLost = false;
|
||||
// Force the framework to rerender the frame.
|
||||
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
void _contextLostListener(DomEvent event) {
|
||||
assert(
|
||||
event.target == _offscreenCanvas || event.target == _canvasElement,
|
||||
'Received a context lost event for a disposed canvas',
|
||||
);
|
||||
_contextLost = true;
|
||||
_forceNewContext = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/// This function is expensive.
|
||||
///
|
||||
/// It's better to reuse canvas if possible.
|
||||
void _createNewCanvas(BitmapSize physicalSize) {
|
||||
// Clear the container, if it's not empty. We're going to create a new <canvas>.
|
||||
if (_offscreenCanvas != null) {
|
||||
_offscreenCanvas!.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false.toJS,
|
||||
);
|
||||
_offscreenCanvas!.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false.toJS,
|
||||
);
|
||||
_offscreenCanvas = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
_cachedContextLostListener = null;
|
||||
} else if (_canvasElement != null) {
|
||||
_canvasElement!.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false.toJS,
|
||||
);
|
||||
_canvasElement!.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false.toJS,
|
||||
);
|
||||
_canvasElement!.remove();
|
||||
_canvasElement = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
_cachedContextLostListener = null;
|
||||
}
|
||||
|
||||
// If `physicalSize` is not precise, use a slightly bigger canvas. This way
|
||||
// we ensure that the rendred picture covers the entire browser window.
|
||||
_pixelWidth = physicalSize.width;
|
||||
_pixelHeight = physicalSize.height;
|
||||
DomEventTarget htmlCanvas;
|
||||
if (useOffscreenCanvas) {
|
||||
final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(
|
||||
_pixelWidth,
|
||||
_pixelHeight,
|
||||
);
|
||||
htmlCanvas = offscreenCanvas;
|
||||
_offscreenCanvas = offscreenCanvas;
|
||||
_canvasElement = null;
|
||||
} else {
|
||||
final DomHTMLCanvasElement canvas = createDomCanvasElement(
|
||||
width: _pixelWidth,
|
||||
height: _pixelHeight,
|
||||
);
|
||||
htmlCanvas = canvas;
|
||||
_canvasElement = canvas;
|
||||
_offscreenCanvas = null;
|
||||
if (isDisplayCanvas) {
|
||||
_canvasElement!.setAttribute('aria-hidden', 'true');
|
||||
_canvasElement!.style.position = 'absolute';
|
||||
hostElement.append(_canvasElement!);
|
||||
_updateLogicalHtmlCanvasSize();
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
_cachedContextRestoredListener = createDomEventListener(_contextRestoredListener);
|
||||
_cachedContextLostListener = createDomEventListener(_contextLostListener);
|
||||
htmlCanvas.addEventListener('webglcontextlost', _cachedContextLostListener, false.toJS);
|
||||
htmlCanvas.addEventListener('webglcontextrestored', _cachedContextRestoredListener, false.toJS);
|
||||
_forceNewContext = false;
|
||||
_contextLost = false;
|
||||
|
||||
if (webGLVersion != -1 && !configuration.canvasKitForceCpuOnly) {
|
||||
var glContext = 0;
|
||||
final 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 (useOffscreenCanvas) {
|
||||
glContext = canvasKit.GetOffscreenWebGLContext(_offscreenCanvas!, options).toInt();
|
||||
} else {
|
||||
glContext = canvasKit.GetWebGLContext(_canvasElement!, options).toInt();
|
||||
}
|
||||
|
||||
_glContext = glContext;
|
||||
|
||||
if (_glContext != 0) {
|
||||
_grContext = canvasKit.MakeGrContext(glContext.toDouble());
|
||||
if (_grContext == null) {
|
||||
// TODO(harryterkelsen): Make this error message more descriptive by
|
||||
// reporting the number of currently live Surfaces, https://github.com/flutter/flutter/issues/162868.
|
||||
throw CanvasKitError(
|
||||
'Failed to initialize CanvasKit. '
|
||||
'CanvasKit.MakeGrContext returned null.',
|
||||
);
|
||||
}
|
||||
if (_sampleCount == -1 || _stencilBits == -1) {
|
||||
_initWebglParams();
|
||||
}
|
||||
// Set the cache byte limit for this grContext, if not specified it will
|
||||
// use CanvasKit's default.
|
||||
_syncCacheBytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _initWebglParams() {
|
||||
WebGLContext gl;
|
||||
if (useOffscreenCanvas) {
|
||||
gl = _offscreenCanvas!.getGlContext(webGLVersion);
|
||||
} else {
|
||||
gl = _canvasElement!.getGlContext(webGLVersion);
|
||||
}
|
||||
_sampleCount = gl.getParameter(gl.samples);
|
||||
_stencilBits = gl.getParameter(gl.stencilBits);
|
||||
}
|
||||
|
||||
CkSurface _createNewSurface(BitmapSize size) {
|
||||
assert(_offscreenCanvas != null || _canvasElement != null);
|
||||
if (webGLVersion == -1) {
|
||||
return _makeSoftwareCanvasSurface('WebGL support not detected', size);
|
||||
} else if (configuration.canvasKitForceCpuOnly) {
|
||||
return _makeSoftwareCanvasSurface('CPU rendering forced by application', size);
|
||||
} else if (_glContext == 0) {
|
||||
return _makeSoftwareCanvasSurface('Failed to initialize WebGL context', size);
|
||||
} else {
|
||||
final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
|
||||
_grContext!,
|
||||
size.width.toDouble(),
|
||||
size.height.toDouble(),
|
||||
SkColorSpaceSRGB,
|
||||
_sampleCount,
|
||||
_stencilBits,
|
||||
);
|
||||
|
||||
if (skSurface == null) {
|
||||
return _makeSoftwareCanvasSurface('Failed to initialize WebGL surface', size);
|
||||
}
|
||||
|
||||
return CkSurface(skSurface, _glContext, size);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _didWarnAboutWebGlInitializationFailure = false;
|
||||
|
||||
/// The underlying GL context. Returns -1 if the context is not initialized.
|
||||
@override
|
||||
@visibleForTesting
|
||||
int get glContext => _glContext;
|
||||
int _glContext = -1;
|
||||
|
||||
/// The canvas object that this surface is rendering to.
|
||||
@visibleForTesting
|
||||
DomEventTarget get canvas => _canvas;
|
||||
late DomEventTarget _canvas;
|
||||
|
||||
void _maybeAttachCanvasToDom();
|
||||
|
||||
/// A [Future] which completes when the [Surface] is initialized and ready to
|
||||
/// render pictures.
|
||||
@override
|
||||
Future<void> get initialized => _initialized.future;
|
||||
final Completer<void> _initialized = Completer<void>();
|
||||
|
||||
late Completer<void>? _handledContextLostEvent;
|
||||
|
||||
/// Creates the canvas object and initializes the graphics context.
|
||||
Future<void> _initialize() async {
|
||||
_createSkiaObjects();
|
||||
_initialized.complete();
|
||||
}
|
||||
|
||||
/// The underlying Skia graphics context.
|
||||
SkGrContext? _grContext;
|
||||
|
||||
void onContextLost() {
|
||||
_handledContextLostEvent?.complete();
|
||||
final DomEventTarget newCanvas = _canvasProvider.acquireCanvas(
|
||||
_currentSize,
|
||||
onContextLost: onContextLost,
|
||||
);
|
||||
recreateContextForCanvas(newCanvas);
|
||||
}
|
||||
|
||||
void _recreateSkSurface() {
|
||||
if (supportsWebGl) {
|
||||
try {
|
||||
_recreateWebGlSkSurface();
|
||||
} catch (e) {
|
||||
_failedToCreateGrContext = true;
|
||||
_fallbackToSoftwareReason = 'failed to create GrContext. Error: $e';
|
||||
_recreateSoftwareSkSurface();
|
||||
}
|
||||
} else {
|
||||
_recreateSoftwareSkSurface();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the GL context and the Skia `GrContext`.
|
||||
void _createGrContext() {
|
||||
if (debugForceGLFailure) {
|
||||
_failedToCreateGrContext = true;
|
||||
_fallbackToSoftwareReason = 'debugForceGLFailure is true';
|
||||
return;
|
||||
}
|
||||
final options = SkWebGLContextOptions(
|
||||
antialias: _kUsingMSAA ? 1 : 0,
|
||||
majorVersion: webGLVersion.toDouble(),
|
||||
);
|
||||
_glContext = _getGlContext(options);
|
||||
_grContext = canvasKit.MakeGrContext(_glContext.toDouble());
|
||||
if (_grContext == null) {
|
||||
_failedToCreateGrContext = true;
|
||||
_fallbackToSoftwareReason = 'failed to create GrContext.';
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the underlying GL context for the canvas.
|
||||
///
|
||||
/// This method is implemented by subclasses to handle their specific
|
||||
/// canvas types.
|
||||
int _getGlContext(SkWebGLContextOptions options);
|
||||
|
||||
/// Creates the Skia objects that are backed by the canvas.
|
||||
///
|
||||
/// This method is responsible for creating the `SkGrContext` and the
|
||||
/// `SkSurface`.
|
||||
void _createSkiaObjects() {
|
||||
if (supportsWebGl) {
|
||||
_createGrContext();
|
||||
}
|
||||
_recreateSkSurface();
|
||||
}
|
||||
|
||||
void _recreateWebGlSkSurface() {
|
||||
_skSurface?.dispose();
|
||||
_skSurface = canvasKit.MakeOnScreenGLSurface(
|
||||
_grContext!,
|
||||
_currentSize.width.toDouble(),
|
||||
_currentSize.height.toDouble(),
|
||||
SkColorSpaceSRGB,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
if (_skSurface == null) {
|
||||
throw Exception('Failed to initialize CanvasKit SkSurface.');
|
||||
}
|
||||
}
|
||||
|
||||
void _recreateSoftwareSkSurface() {
|
||||
CkSurface _makeSoftwareCanvasSurface(String reason, BitmapSize size) {
|
||||
if (!_didWarnAboutWebGlInitializationFailure) {
|
||||
printWarning('WARNING: Falling back to CPU-only rendering. $reason.');
|
||||
_didWarnAboutWebGlInitializationFailure = true;
|
||||
printWarning(
|
||||
'WARNING: Falling back to CPU-only rendering. Reason: $_fallbackToSoftwareReason',
|
||||
);
|
||||
}
|
||||
_skSurface?.dispose();
|
||||
_skSurface = _createSoftwareSkSurface();
|
||||
if (_skSurface == null) {
|
||||
throw Exception('Failed to initialize CanvasKit SkSurface.');
|
||||
|
||||
try {
|
||||
assert(!debugThrowOnSoftwareSurfaceCreation);
|
||||
|
||||
SkSurface surface;
|
||||
if (useOffscreenCanvas) {
|
||||
surface = canvasKit.MakeOffscreenSWCanvasSurface(_offscreenCanvas!);
|
||||
} else {
|
||||
surface = canvasKit.MakeSWCanvasSurface(_canvasElement!);
|
||||
}
|
||||
return CkSurface(surface, null, size);
|
||||
} catch (error) {
|
||||
throw CanvasKitError('Failed to create CPU-based surface: $error.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an SkSurface for software rendering. This is used when WebGl is not
|
||||
/// supported or when it fails to initialize.
|
||||
SkSurface _createSoftwareSkSurface();
|
||||
|
||||
double _currentDevicePixelRatio = -1;
|
||||
@override
|
||||
bool get isConnected => _canvasElement!.isConnected!;
|
||||
|
||||
@override
|
||||
Future<void> setSize(BitmapSize size) async {
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
if (_skSurface != null &&
|
||||
_currentSize == size &&
|
||||
devicePixelRatio == _currentDevicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_currentDevicePixelRatio = devicePixelRatio;
|
||||
_currentSize = size;
|
||||
_canvasProvider.resizeCanvas(canvas, size);
|
||||
_recreateSkSurface();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> recreateContextForCanvas(DomEventTarget newCanvas) async {
|
||||
// The old Skia surface is now invalid and should be disposed.
|
||||
_skSurface?.dispose();
|
||||
_skSurface = null;
|
||||
|
||||
// The GrContext is also invalid and will be recreated by `_createSkiaObjects`.
|
||||
_grContext = null;
|
||||
|
||||
_canvas = newCanvas;
|
||||
_maybeAttachCanvasToDom();
|
||||
_createSkiaObjects();
|
||||
void initialize() {
|
||||
ensureSurface();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_skSurface?.dispose();
|
||||
_offscreenCanvas?.removeEventListener(
|
||||
'webglcontextlost',
|
||||
_cachedContextLostListener,
|
||||
false.toJS,
|
||||
);
|
||||
_offscreenCanvas?.removeEventListener(
|
||||
'webglcontextrestored',
|
||||
_cachedContextRestoredListener,
|
||||
false.toJS,
|
||||
);
|
||||
_cachedContextLostListener = null;
|
||||
_cachedContextRestoredListener = null;
|
||||
_surface?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setSkiaResourceCacheMaxBytes(int bytes) {
|
||||
_grContext?.setResourceCacheLimitBytes(bytes.toDouble());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ByteData?> rasterizeImage(ui.Image image, ui.ImageByteFormat format) async {
|
||||
await _initialized.future;
|
||||
final ckImage = image as CkImage;
|
||||
final SkSurface skSurface = _skSurface!;
|
||||
final canvas = CkCanvas.fromSkCanvas(skSurface.getCanvas());
|
||||
canvas.drawImage(ckImage, ui.Offset.zero, ui.Paint());
|
||||
final SkImage snapshot = skSurface.makeImageSnapshot();
|
||||
final Uint8List? bytes = snapshot.encodeToBytes();
|
||||
snapshot.delete();
|
||||
return bytes?.buffer.asByteData();
|
||||
}
|
||||
|
||||
@override
|
||||
DomCanvasImageSource get canvasImageSource => canvas as DomCanvasImageSource;
|
||||
|
||||
@override
|
||||
Future<void> rasterizeToCanvas(ui.Picture picture) async {
|
||||
await _initialized.future;
|
||||
final canvas = CkCanvas.fromSkCanvas(_skSurface!.getCanvas());
|
||||
final ckPicture = picture as CkPicture;
|
||||
canvas.clear(const ui.Color(0x00000000));
|
||||
canvas.drawPicture(ckPicture);
|
||||
_skSurface!.flush();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> triggerContextLoss();
|
||||
|
||||
@override
|
||||
Future<void> get handledContextLossEvent => _handledContextLostEvent!.future;
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// The CanvasKit implementation of [OffscreenSurface].
|
||||
class CkOffscreenSurface extends CkSurface implements OffscreenSurface {
|
||||
CkOffscreenSurface(OffscreenCanvasProvider super.canvasProvider);
|
||||
/// A Dart wrapper around Skia's SkSurface.
|
||||
class CkSurface {
|
||||
CkSurface(this.surface, this._glContext, this._size);
|
||||
|
||||
@override
|
||||
int _getGlContext(SkWebGLContextOptions options) {
|
||||
return canvasKit.GetOffscreenWebGLContext(canvas as DomOffscreenCanvas, options).toInt();
|
||||
CkCanvas getCanvas() {
|
||||
assert(!_isDisposed, 'Attempting to use the canvas of a disposed surface');
|
||||
return CkCanvas.fromSkCanvas(surface.getCanvas());
|
||||
}
|
||||
|
||||
@override
|
||||
SkSurface _createSoftwareSkSurface() {
|
||||
return canvasKit.MakeOffscreenSWCanvasSurface(canvas as DomOffscreenCanvas);
|
||||
/// The underlying CanvasKit surface object.
|
||||
///
|
||||
/// Only borrow this value temporarily. Do not store it as it may be deleted
|
||||
/// at any moment. Storing it may lead to dangling pointer bugs.
|
||||
final SkSurface surface;
|
||||
|
||||
final BitmapSize _size;
|
||||
|
||||
final int? _glContext;
|
||||
|
||||
/// Flushes the graphics to be rendered on screen.
|
||||
void flush() {
|
||||
surface.flush();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DomImageBitmap>> rasterizeToImageBitmaps(List<ui.Picture> pictures) async {
|
||||
await _initialized.future;
|
||||
final bitmaps = <DomImageBitmap>[];
|
||||
for (final picture in pictures) {
|
||||
await rasterizeToCanvas(picture);
|
||||
bitmaps.add(await createImageBitmap(_canvas));
|
||||
int? get context => _glContext;
|
||||
|
||||
int width() => surface.width().ceil();
|
||||
int height() => surface.height().ceil();
|
||||
|
||||
void dispose() {
|
||||
if (_isDisposed) {
|
||||
return;
|
||||
}
|
||||
return bitmaps;
|
||||
surface.dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void _maybeAttachCanvasToDom() {
|
||||
// Do not attach the OffscreenCanvas to the DOM.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> triggerContextLoss() async {
|
||||
_handledContextLostEvent = Completer<void>();
|
||||
final WebGLContext gl = (canvas as DomOffscreenCanvas).getGlContext(webGLVersion);
|
||||
gl.loseContextExtension.loseContext();
|
||||
}
|
||||
}
|
||||
|
||||
/// The CanvasKit implementation of [OnscreenSurface].
|
||||
class CkOnscreenSurface extends CkSurface implements OnscreenSurface {
|
||||
CkOnscreenSurface(OnscreenCanvasProvider super.canvasProvider);
|
||||
|
||||
@override
|
||||
int _getGlContext(SkWebGLContextOptions options) {
|
||||
return canvasKit.GetWebGLContext(canvas as DomHTMLCanvasElement, options).toInt();
|
||||
}
|
||||
|
||||
@override
|
||||
SkSurface _createSoftwareSkSurface() {
|
||||
return canvasKit.MakeSWCanvasSurface(canvas as DomHTMLCanvasElement);
|
||||
}
|
||||
|
||||
final DomElement _hostElement = createDomElement('flt-canvas-container');
|
||||
|
||||
@override
|
||||
DomElement get hostElement => _hostElement;
|
||||
|
||||
@override
|
||||
void _maybeAttachCanvasToDom() {
|
||||
hostElement.appendChild(canvas as DomHTMLCanvasElement);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isConnected =>
|
||||
((canvas as JSAny?).isA<DomHTMLCanvasElement>()) &&
|
||||
(canvas as DomHTMLCanvasElement).isConnected!;
|
||||
|
||||
@override
|
||||
void initialize() {
|
||||
// No extra initialization is required.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> triggerContextLoss() async {
|
||||
_handledContextLostEvent = Completer<void>();
|
||||
final WebGLContext gl = (canvas as DomHTMLCanvasElement).getGlContext(webGLVersion);
|
||||
gl.loseContextExtension.loseContext();
|
||||
}
|
||||
bool _isDisposed = false;
|
||||
}
|
||||
|
||||
@ -1,110 +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:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
/// Manages the lifecycle of raw canvas elements, abstracting away the differences
|
||||
/// between onscreen and offscreen canvases.
|
||||
///
|
||||
/// This class is responsible for:
|
||||
/// - Acquiring and releasing canvas elements.
|
||||
/// - Resizing canvases.
|
||||
/// - Attaching `webglcontextlost` event listeners and notifying the consumer.
|
||||
abstract class CanvasProvider<C extends DomEventTarget> {
|
||||
final Map<C, DomEventListener> _eventListeners = <C, DomEventListener>{};
|
||||
|
||||
/// Acquires a canvas element of a given `size`.
|
||||
///
|
||||
/// The `onContextLost` callback will be invoked when the underlying rendering
|
||||
/// context for this canvas is lost.
|
||||
C acquireCanvas(BitmapSize size, {required ui.VoidCallback onContextLost}) {
|
||||
final C canvas = _createCanvas(size);
|
||||
final DomEventListener eventListener = createDomEventListener((DomEvent event) {
|
||||
onContextLost();
|
||||
// The canvas is no longer usable.
|
||||
releaseCanvas(canvas);
|
||||
});
|
||||
|
||||
_eventListeners[canvas] = eventListener;
|
||||
canvas.addEventListener('webglcontextlost', eventListener);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/// Resizes the `canvas` element to the new `size`.
|
||||
///
|
||||
/// This method is responsible for updating the canvas element's dimensions
|
||||
/// and any associated properties (e.g., CSS styles for onscreen canvases).
|
||||
void resizeCanvas(C canvas, BitmapSize size);
|
||||
|
||||
/// Releases a `canvas` element, allowing it to be pooled or disposed of.
|
||||
void releaseCanvas(C canvas) {
|
||||
final DomEventListener? listener = _eventListeners.remove(canvas);
|
||||
if (listener != null) {
|
||||
canvas.removeEventListener('webglcontextlost', listener);
|
||||
}
|
||||
detachCanvas(canvas);
|
||||
}
|
||||
|
||||
/// Disposes of all canvases managed by this provider.
|
||||
void dispose() {
|
||||
List<C>.from(_eventListeners.keys).forEach(releaseCanvas);
|
||||
assert(_eventListeners.isEmpty);
|
||||
}
|
||||
|
||||
/// Creates a canvas element.
|
||||
C _createCanvas(BitmapSize size);
|
||||
|
||||
/// Detaches a canvas element from the DOM if necessary.
|
||||
void detachCanvas(C canvas);
|
||||
}
|
||||
|
||||
/// A [CanvasProvider] that manages a pool of [dom.DomOffscreenCanvas] elements.
|
||||
class OffscreenCanvasProvider extends CanvasProvider<DomOffscreenCanvas> {
|
||||
@override
|
||||
DomOffscreenCanvas _createCanvas(BitmapSize size) {
|
||||
return DomOffscreenCanvas(size.width, size.height);
|
||||
}
|
||||
|
||||
@override
|
||||
void detachCanvas(DomOffscreenCanvas canvas) {
|
||||
// Nothing to do for offscreen canvases.
|
||||
}
|
||||
|
||||
@override
|
||||
void resizeCanvas(DomOffscreenCanvas canvas, BitmapSize size) {
|
||||
canvas.width = size.width.toDouble();
|
||||
canvas.height = size.height.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [CanvasProvider] that manages [dom.DomHTMLCanvasElement] elements.
|
||||
class OnscreenCanvasProvider extends CanvasProvider<DomHTMLCanvasElement> {
|
||||
@override
|
||||
DomHTMLCanvasElement _createCanvas(BitmapSize size) {
|
||||
final DomHTMLCanvasElement canvas = createDomCanvasElement();
|
||||
resizeCanvas(canvas, size);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@override
|
||||
void detachCanvas(DomHTMLCanvasElement canvas) {
|
||||
canvas.remove();
|
||||
}
|
||||
|
||||
@override
|
||||
void resizeCanvas(DomHTMLCanvasElement canvas, BitmapSize size) {
|
||||
canvas.width = size.width.toDouble();
|
||||
canvas.height = size.height.toDouble();
|
||||
|
||||
// When using an onscreen canvas, we also need to update the CSS size to
|
||||
// account for the device pixel ratio.
|
||||
final double ratio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
final cssWidth = '${size.width / ratio}px';
|
||||
final cssHeight = '${size.height / ratio}px';
|
||||
canvas.style
|
||||
..width = cssWidth
|
||||
..height = cssHeight;
|
||||
}
|
||||
}
|
||||
@ -1,94 +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:meta/meta.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
/// A [Rasterizer] which uses one or many on-screen WebGL contexts to display
|
||||
/// the scene. This way of rendering is prone to bugs because there is a limit
|
||||
/// to how many WebGL contexts can be live at one time as well as bugs in
|
||||
/// sharing GL resources between the contexts. However, using
|
||||
/// [createImageBitmap] is currently very slow on Firefox and Safari browsers,
|
||||
/// so directly rendering to several [Surface]s is how we can achieve 60 fps on
|
||||
/// these browsers.
|
||||
class MultiSurfaceRasterizer extends Rasterizer {
|
||||
MultiSurfaceRasterizer(OnscreenSurface Function(OnscreenCanvasProvider) onscreenSurfaceCreateFn)
|
||||
: _surfaceProvider = OnscreenSurfaceProvider(OnscreenCanvasProvider(), onscreenSurfaceCreateFn);
|
||||
|
||||
final OnscreenSurfaceProvider _surfaceProvider;
|
||||
|
||||
@override
|
||||
@visibleForTesting
|
||||
SurfaceProvider get surfaceProvider => _surfaceProvider;
|
||||
|
||||
@override
|
||||
MultiSurfaceViewRasterizer createViewRasterizer(EngineFlutterView view) {
|
||||
return _viewRasterizers.putIfAbsent(
|
||||
view,
|
||||
() => MultiSurfaceViewRasterizer(view, this, _surfaceProvider),
|
||||
);
|
||||
}
|
||||
|
||||
final Map<EngineFlutterView, MultiSurfaceViewRasterizer> _viewRasterizers =
|
||||
<EngineFlutterView, MultiSurfaceViewRasterizer>{};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final MultiSurfaceViewRasterizer viewRasterizer in _viewRasterizers.values) {
|
||||
viewRasterizer.dispose();
|
||||
}
|
||||
_viewRasterizers.clear();
|
||||
_surfaceProvider.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void setResourceCacheMaxBytes(int bytes) {
|
||||
_surfaceProvider.setSkiaResourceCacheMaxBytes(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Surface createPictureToImageSurface() {
|
||||
return _surfaceProvider.createSurface();
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSurfaceViewRasterizer extends ViewRasterizer {
|
||||
MultiSurfaceViewRasterizer(super.view, this.rasterizer, this.surfaceProvider);
|
||||
|
||||
final MultiSurfaceRasterizer rasterizer;
|
||||
final OnscreenSurfaceProvider surfaceProvider;
|
||||
|
||||
@override
|
||||
late final DisplayCanvasFactory<OnscreenSurface> displayFactory =
|
||||
DisplayCanvasFactory<OnscreenSurface>(createCanvas: surfaceProvider.createSurface);
|
||||
|
||||
@override
|
||||
Future<void> prepareToDraw() {
|
||||
return displayFactory.baseCanvas.setSize(currentFrameSize);
|
||||
}
|
||||
|
||||
Future<void> rasterizeToCanvas(OnscreenSurface canvas, ui.Picture picture) {
|
||||
canvas.setSize(currentFrameSize);
|
||||
return canvas.rasterizeToCanvas(picture);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rasterize(
|
||||
List<DisplayCanvas> displayCanvases,
|
||||
List<ui.Picture> pictures,
|
||||
FrameTimingRecorder? recorder,
|
||||
) async {
|
||||
if (displayCanvases.length != pictures.length) {
|
||||
throw ArgumentError('Called rasterize() with a different number of canvases and pictures.');
|
||||
}
|
||||
recorder?.recordRasterStart();
|
||||
final rasterizeFutures = <Future<void>>[];
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
rasterizeFutures.add(rasterizeToCanvas(displayCanvases[i] as OnscreenSurface, pictures[i]));
|
||||
}
|
||||
await Future.wait<void>(rasterizeFutures);
|
||||
recorder?.recordRasterFinish();
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@
|
||||
// found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
@ -13,17 +12,11 @@ abstract class Rasterizer {
|
||||
/// Creates a [ViewRasterizer] for a given [view].
|
||||
ViewRasterizer createViewRasterizer(EngineFlutterView view);
|
||||
|
||||
/// Creates a [Surface] which is to be used for [Picture.toImage] calls.
|
||||
Surface createPictureToImageSurface();
|
||||
|
||||
/// Sets the maximum size of the resource cache to [bytes].
|
||||
void setResourceCacheMaxBytes(int bytes);
|
||||
|
||||
/// Disposes this rasterizer and all [ViewRasterizer]s that it created.
|
||||
void dispose();
|
||||
|
||||
@visibleForTesting
|
||||
SurfaceProvider get surfaceProvider;
|
||||
}
|
||||
|
||||
/// Composites Flutter content into a [FlutterView]. Manages the creation of
|
||||
@ -80,7 +73,7 @@ abstract class ViewRasterizer {
|
||||
final bitmapSize = BitmapSize.fromSize(frameSize);
|
||||
|
||||
currentFrameSize = bitmapSize;
|
||||
await prepareToDraw();
|
||||
prepareToDraw();
|
||||
viewEmbedder.frameSize = currentFrameSize;
|
||||
final Frame compositorFrame = context.acquireFrame(viewEmbedder);
|
||||
|
||||
@ -94,7 +87,7 @@ abstract class ViewRasterizer {
|
||||
///
|
||||
/// For example, in the [OffscreenCanvasRasterizer], this ensures the backing
|
||||
/// [OffscreenCanvas] is the correct size to draw the frame.
|
||||
Future<void> prepareToDraw();
|
||||
void prepareToDraw();
|
||||
|
||||
/// Rasterizes the given [pictures] into the [displayCanvases].
|
||||
///
|
||||
@ -129,6 +122,7 @@ abstract class ViewRasterizer {
|
||||
/// Disposes this rasterizer.
|
||||
void dispose() {
|
||||
viewEmbedder.dispose();
|
||||
displayFactory.dispose();
|
||||
}
|
||||
|
||||
/// Clears the state. Used in tests.
|
||||
|
||||
@ -1,115 +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:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../dom.dart';
|
||||
import '../util.dart';
|
||||
import 'canvas_provider.dart';
|
||||
import 'rasterizer.dart';
|
||||
|
||||
/// A class which provides and manages [Surface] objects.
|
||||
abstract class SurfaceProvider<C extends Surface, D extends CanvasProvider> {
|
||||
SurfaceProvider(this._canvasProvider, this._surfaceCreateFn);
|
||||
|
||||
final D _canvasProvider;
|
||||
final C Function(D) _surfaceCreateFn;
|
||||
|
||||
final List<C> _createdSurfaces = <C>[];
|
||||
|
||||
C createSurface() {
|
||||
final C surface = _surfaceCreateFn(_canvasProvider);
|
||||
if (_resourceCacheMaxBytes != null) {
|
||||
surface.setSkiaResourceCacheMaxBytes(_resourceCacheMaxBytes!);
|
||||
}
|
||||
_createdSurfaces.add(surface);
|
||||
return surface;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final C surface in _createdSurfaces) {
|
||||
surface.dispose();
|
||||
}
|
||||
_createdSurfaces.clear();
|
||||
}
|
||||
|
||||
int? _resourceCacheMaxBytes;
|
||||
|
||||
void setSkiaResourceCacheMaxBytes(int bytes) {
|
||||
_resourceCacheMaxBytes = bytes;
|
||||
for (final C surface in _createdSurfaces) {
|
||||
surface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [SurfaceProvider] that creates [OffscreenSurface] objects.
|
||||
class OffscreenSurfaceProvider extends SurfaceProvider<OffscreenSurface, OffscreenCanvasProvider> {
|
||||
OffscreenSurfaceProvider(super.canvasProvider, super.surfaceCreateFn);
|
||||
}
|
||||
|
||||
/// A [SurfaceProvider] that creates [OnscreenSurface] objects.
|
||||
class OnscreenSurfaceProvider extends SurfaceProvider<OnscreenSurface, OnscreenCanvasProvider> {
|
||||
OnscreenSurfaceProvider(super.canvasProvider, super.surfaceCreateFn);
|
||||
}
|
||||
|
||||
/// The base interface for a rendering surface.
|
||||
abstract class Surface {
|
||||
/// Sets the size of the underlying canvas.
|
||||
Future<void> setSize(BitmapSize size);
|
||||
|
||||
/// Converts a `ui.Image` into a `ByteData` object in the specified format.
|
||||
Future<ByteData?> rasterizeImage(ui.Image image, ui.ImageByteFormat format);
|
||||
|
||||
/// Sets the maximum number of bytes for the GPU resource cache.
|
||||
void setSkiaResourceCacheMaxBytes(int bytes);
|
||||
|
||||
/// Discards the old graphics context and creates a new one using the
|
||||
/// provided canvas object.
|
||||
///
|
||||
/// This is called by the `SurfaceManager` in response to a
|
||||
/// `webglcontextlost` event.
|
||||
Future<void> recreateContextForCanvas(DomEventTarget newCanvas);
|
||||
|
||||
/// Disposes of the surface and its resources.
|
||||
void dispose();
|
||||
|
||||
/// A [Future] which completes when the [Surface] is initialized and ready to
|
||||
/// render pictures.
|
||||
Future<void> get initialized;
|
||||
|
||||
/// The underlying canvas used to render the pixels.
|
||||
DomCanvasImageSource get canvasImageSource;
|
||||
|
||||
/// Rasterizes the given [picture] to this canvas.
|
||||
Future<void> rasterizeToCanvas(ui.Picture picture);
|
||||
|
||||
@visibleForTesting
|
||||
int get glContext;
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> triggerContextLoss();
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> get handledContextLossEvent;
|
||||
}
|
||||
|
||||
/// A rendering surface that is optimized for producing `DomImageBitmap` objects.
|
||||
///
|
||||
/// This surface is not attached to the DOM and is used for off-screen rendering.
|
||||
abstract class OffscreenSurface extends Surface {
|
||||
/// Rasterizes the given list of [pictures] into a list of `DomImageBitmap`
|
||||
/// objects.
|
||||
Future<List<DomImageBitmap>> rasterizeToImageBitmaps(List<ui.Picture> pictures);
|
||||
}
|
||||
|
||||
/// A rendering surface that is also a `DisplayCanvas`.
|
||||
///
|
||||
/// This surface renders a picture directly to an on-screen canvas that is
|
||||
/// part of the DOM.
|
||||
abstract class OnscreenSurface extends Surface implements DisplayCanvas {}
|
||||
@ -178,6 +178,12 @@ extension type DomWindow._(JSObject _) implements DomEventTarget {
|
||||
/// The Trusted Types API (when available).
|
||||
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API
|
||||
external DomTrustedTypePolicyFactory? get trustedTypes;
|
||||
|
||||
@JS('createImageBitmap')
|
||||
external JSPromise<JSAny?> _createImageBitmap(DomImageData source);
|
||||
Future<DomImageBitmap> createImageBitmap(DomImageData source) {
|
||||
return _createImageBitmap(source).toDart.then((JSAny? value) => value! as DomImageBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
typedef DomRequestAnimationFrameCallback = void Function(JSNumber highResTime);
|
||||
@ -214,9 +220,6 @@ Future<DomImageBitmap> createImageBitmap(
|
||||
JSAny source, [
|
||||
({int x, int y, int width, int height})? bounds,
|
||||
]) {
|
||||
if (debugThrowOnCreateImageBitmapIfDisabled && !browserSupportsCreateImageBitmap) {
|
||||
throw UnsupportedError('createImageBitmap is not supported in this browser');
|
||||
}
|
||||
JSPromise<JSAny?> jsPromise;
|
||||
if (bounds != null) {
|
||||
jsPromise = _createImageBitmap(source, bounds.x, bounds.y, bounds.width, bounds.height);
|
||||
@ -430,7 +433,6 @@ extension type DomElement._(JSObject _) implements DomNode {
|
||||
external String? getAttribute(String attributeName);
|
||||
external DomRect getBoundingClientRect();
|
||||
external void prepend(DomNode node);
|
||||
external void replaceWith(DomNode node);
|
||||
external DomElement? querySelector(String selectors);
|
||||
external DomElement? closest(String selectors);
|
||||
external bool matches(String selectors);
|
||||
@ -802,7 +804,7 @@ extension type DomPerformanceEntry._(JSObject _) implements JSObject {}
|
||||
extension type DomPerformanceMeasure._(JSObject _) implements DomPerformanceEntry {}
|
||||
|
||||
@JS('HTMLCanvasElement')
|
||||
extension type DomHTMLCanvasElement._(JSObject _) implements DomHTMLElement, DomCanvasImageSource {
|
||||
extension type DomHTMLCanvasElement._(JSObject _) implements DomHTMLElement {
|
||||
external double? width;
|
||||
external double? height;
|
||||
|
||||
@ -2020,7 +2022,7 @@ DomHTMLLabelElement createDomHTMLLabelElement() =>
|
||||
domDocument.createElement('label') as DomHTMLLabelElement;
|
||||
|
||||
@JS('OffscreenCanvas')
|
||||
extension type DomOffscreenCanvas._(JSObject _) implements DomEventTarget, DomCanvasImageSource {
|
||||
extension type DomOffscreenCanvas._(JSObject _) implements DomEventTarget {
|
||||
external DomOffscreenCanvas(int width, int height);
|
||||
|
||||
external double? height;
|
||||
@ -2605,16 +2607,14 @@ external JSAny? get _offscreenCanvasConstructor;
|
||||
|
||||
bool browserSupportsOffscreenCanvas = _offscreenCanvasConstructor != null;
|
||||
|
||||
@JS('window.createImageBitmap')
|
||||
external JSAny? get _createImageBitmapFunction;
|
||||
|
||||
/// Set to `true` to disable `createImageBitmap` support. Used in tests.
|
||||
@visibleForTesting
|
||||
bool debugDisableCreateImageBitmapSupport = false;
|
||||
|
||||
/// Set to `true` to throw an error if `createImageBitmap` is disabled. Used in tests.
|
||||
@visibleForTesting
|
||||
bool debugThrowOnCreateImageBitmapIfDisabled = false;
|
||||
|
||||
bool get browserSupportsCreateImageBitmap =>
|
||||
domWindow.has('createImageBitmap') &&
|
||||
_createImageBitmapFunction != null &&
|
||||
!isChrome110OrOlder &&
|
||||
!debugDisableCreateImageBitmapSupport;
|
||||
|
||||
|
||||
@ -36,9 +36,6 @@ abstract class LayerPicture implements ui.Picture {
|
||||
///
|
||||
/// The copy points to the same underlying Skia picture as this picture.
|
||||
LayerPicture clone();
|
||||
|
||||
/// Returns `true` if the picture has been disposed.
|
||||
bool get isDisposed;
|
||||
}
|
||||
|
||||
/// A [ui.PictureRecorder] which allows callers to know if it has been disposed.
|
||||
|
||||
@ -408,12 +408,6 @@ class MeasureVisitor extends LayerVisitor<void> {
|
||||
@override
|
||||
void visitPicture(PictureLayer picture) {
|
||||
assert(picture.needsPainting);
|
||||
if (picture.picture.isDisposed) {
|
||||
// The picture layer was disposed before the picture could be painted.
|
||||
// Just ignore it then.
|
||||
picture.isCulled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
measuringCanvas.save();
|
||||
measuringCanvas.translate(picture.offset.dx, picture.offset.dy);
|
||||
@ -678,13 +672,6 @@ class PaintVisitor extends LayerVisitor<void> {
|
||||
void visitPicture(PictureLayer picture) {
|
||||
assert(picture.needsPainting);
|
||||
|
||||
if (picture.picture.isDisposed) {
|
||||
// The picture layer was disposed before the picture could be painted.
|
||||
// Just ignore it then.
|
||||
picture.isCulled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// For each shader mask this picture is a child of, record that it needs
|
||||
// to have the shader mask applied to it.
|
||||
for (final ShaderMaskEngineLayer shaderMask in shaderMaskStack) {
|
||||
|
||||
@ -44,9 +44,6 @@ abstract class Renderer {
|
||||
|
||||
late Rasterizer rasterizer;
|
||||
|
||||
/// A surface used specifically for `Picture.toImage`.
|
||||
Surface get pictureToImageSurface;
|
||||
|
||||
/// Resets the [Rasterizer] to the default value. Used in tests.
|
||||
@visibleForTesting
|
||||
void debugResetRasterizer();
|
||||
@ -339,8 +336,11 @@ abstract class Renderer {
|
||||
void dispose() {
|
||||
_onViewCreatedListener.cancel();
|
||||
_onViewDisposedListener.cancel();
|
||||
for (final ViewRasterizer rasterizer in rasterizers.values) {
|
||||
rasterizer.dispose();
|
||||
}
|
||||
rasterizers.clear();
|
||||
rasterizer.dispose();
|
||||
pictureToImageSurface.dispose();
|
||||
}
|
||||
|
||||
/// Clears the state of this renderer. Used in tests.
|
||||
|
||||
@ -14,6 +14,7 @@ export 'skwasm_impl/filters.dart';
|
||||
export 'skwasm_impl/font_collection.dart';
|
||||
export 'skwasm_impl/image.dart';
|
||||
export 'skwasm_impl/memory.dart';
|
||||
export 'skwasm_impl/offscreen_canvas_rasterizer.dart';
|
||||
export 'skwasm_impl/paint.dart';
|
||||
export 'skwasm_impl/paragraph.dart';
|
||||
export 'skwasm_impl/path.dart';
|
||||
|
||||
@ -21,7 +21,7 @@ class SkwasmBrowserImageDecoder extends BrowserImageDecoder {
|
||||
ui.Image generateImageFromVideoFrame(VideoFrame frame) {
|
||||
final int width = frame.displayWidth.toInt();
|
||||
final int height = frame.displayHeight.toInt();
|
||||
final surface = renderer.pictureToImageSurface as SkwasmSurface;
|
||||
final SkwasmSurface surface = (renderer as SkwasmRenderer).surface;
|
||||
return SkwasmImage(imageCreateFromTextureSource(frame, width, height, surface.handle));
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,11 +60,11 @@ class SkwasmImage extends SkwasmObjectWrapper<RawImage> implements ui.Image {
|
||||
final canvas = ui.Canvas(recorder);
|
||||
canvas.drawImage(this, ui.Offset.zero, ui.Paint());
|
||||
final picture = recorder.endRecording() as SkwasmPicture;
|
||||
final surface = renderer.pictureToImageSurface as SkwasmSurface;
|
||||
await surface.setSize(BitmapSize(width, height));
|
||||
final DomImageBitmap bitmap = (await surface.rasterizeToImageBitmaps(<SkwasmPicture>[
|
||||
picture,
|
||||
])).single;
|
||||
final DomImageBitmap bitmap = (await (renderer as SkwasmRenderer).surface.renderPictures(
|
||||
<SkwasmPicture>[picture],
|
||||
picture.cullRect.width.ceil(),
|
||||
picture.cullRect.height.ceil(),
|
||||
)).imageBitmaps.single;
|
||||
final DomOffscreenCanvas offscreenCanvas = createDomOffscreenCanvas(
|
||||
bitmap.width,
|
||||
bitmap.height,
|
||||
@ -80,7 +80,7 @@ class SkwasmImage extends SkwasmObjectWrapper<RawImage> implements ui.Image {
|
||||
context.transferFromImageBitmap(null);
|
||||
return ByteData.view(arrayBuffer.toDart);
|
||||
} else {
|
||||
return renderer.pictureToImageSurface.rasterizeImage(this, format);
|
||||
return (renderer as SkwasmRenderer).surface.rasterizeImage(this, format);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
// 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:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../skwasm_impl.dart';
|
||||
|
||||
/// A [Rasterizer] that uses a single GL context in an OffscreenCanvas to do
|
||||
/// all the rendering. It transfers bitmaps created in the OffscreenCanvas to
|
||||
/// one or many on-screen <canvas> elements to actually display the scene.
|
||||
class SkwasmOffscreenCanvasRasterizer extends Rasterizer {
|
||||
SkwasmOffscreenCanvasRasterizer(this.offscreenSurface);
|
||||
|
||||
/// This is an SkSurface backed by an OffScreenCanvas. This single Surface is
|
||||
/// used to render to many RenderCanvases to produce the rendered scene.
|
||||
final SkwasmSurface offscreenSurface;
|
||||
|
||||
@override
|
||||
SkwasmOffscreenCanvasViewRasterizer createViewRasterizer(EngineFlutterView view) {
|
||||
return _viewRasterizers.putIfAbsent(
|
||||
view,
|
||||
() => SkwasmOffscreenCanvasViewRasterizer(view, this),
|
||||
);
|
||||
}
|
||||
|
||||
final Map<EngineFlutterView, SkwasmOffscreenCanvasViewRasterizer> _viewRasterizers =
|
||||
<EngineFlutterView, SkwasmOffscreenCanvasViewRasterizer>{};
|
||||
|
||||
@override
|
||||
void setResourceCacheMaxBytes(int bytes) {
|
||||
offscreenSurface.setSkiaResourceCacheMaxBytes(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
offscreenSurface.dispose();
|
||||
for (final SkwasmOffscreenCanvasViewRasterizer viewRasterizer in _viewRasterizers.values) {
|
||||
viewRasterizer.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SkwasmOffscreenCanvasViewRasterizer extends ViewRasterizer {
|
||||
SkwasmOffscreenCanvasViewRasterizer(super.view, this.rasterizer);
|
||||
|
||||
final SkwasmOffscreenCanvasRasterizer rasterizer;
|
||||
|
||||
@override
|
||||
final DisplayCanvasFactory<RenderCanvas> displayFactory = DisplayCanvasFactory<RenderCanvas>(
|
||||
createCanvas: () => RenderCanvas(),
|
||||
);
|
||||
|
||||
@override
|
||||
void prepareToDraw() {
|
||||
// No need to do anything here. Skwasm sizes the surface in the `rasterize`
|
||||
// call below.
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> rasterize(
|
||||
List<DisplayCanvas> displayCanvases,
|
||||
List<ui.Picture> pictures,
|
||||
FrameTimingRecorder? recorder,
|
||||
) async {
|
||||
if (displayCanvases.length != pictures.length) {
|
||||
throw ArgumentError('Called rasterize() with a different number of canvases and pictures.');
|
||||
}
|
||||
final RenderResult renderResult = await rasterizer.offscreenSurface.renderPictures(
|
||||
pictures.cast<SkwasmPicture>(),
|
||||
currentFrameSize.width,
|
||||
currentFrameSize.height,
|
||||
);
|
||||
recorder?.recordRasterStart(renderResult.rasterStartMicros);
|
||||
recorder?.recordRasterFinish(renderResult.rasterEndMicros);
|
||||
for (var i = 0; i < displayCanvases.length; i++) {
|
||||
final renderCanvas = displayCanvases[i] as RenderCanvas;
|
||||
final DomImageBitmap bitmap = renderResult.imageBitmaps[i];
|
||||
renderCanvas.render(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,14 +49,6 @@ class SkwasmPicture extends SkwasmObjectWrapper<RawPicture> implements LayerPict
|
||||
pictureRef(handle);
|
||||
return SkwasmPicture.fromHandle(handle, isClone: true);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SkwasmPicture(${handle.address})';
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isDisposed => debugDisposed;
|
||||
}
|
||||
|
||||
class SkwasmPictureRecorder extends SkwasmObjectWrapper<RawPictureRecorder>
|
||||
|
||||
@ -5,10 +5,7 @@
|
||||
@DefaultAsset('skwasm')
|
||||
library skwasm_impl;
|
||||
|
||||
import 'dart:_wasm';
|
||||
import 'dart:ffi';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:ui/src/engine/skwasm/skwasm_impl.dart';
|
||||
|
||||
final class RawSurface extends Opaque {}
|
||||
@ -24,25 +21,9 @@ typedef CallbackId = int;
|
||||
@Native<SurfaceHandle Function()>(symbol: 'surface_create', isLeaf: true)
|
||||
external SurfaceHandle surfaceCreate();
|
||||
|
||||
// We use a wasm import directly here instead of @Native since this uses an externref
|
||||
// in the function signature.
|
||||
CallbackId surfaceSetCanvas(SurfaceHandle handle, JSAny canvas) =>
|
||||
surfaceSetCanvasImpl(handle.address.toWasmI32(), externRefForJSAny(canvas)).toIntUnsigned();
|
||||
@pragma('wasm:import', 'skwasm.surface_setCanvas')
|
||||
external WasmI32 surfaceSetCanvasImpl(WasmI32 surfaceHandle, WasmExternRef? frame);
|
||||
|
||||
@Native<Int32 Function(SurfaceHandle, Int, Int)>(symbol: 'surface_setSize', isLeaf: true)
|
||||
external CallbackId surfaceSetSize(SurfaceHandle surface, int width, int height);
|
||||
|
||||
@Native<UnsignedLong Function(SurfaceHandle)>(symbol: 'surface_getThreadId', isLeaf: true)
|
||||
external int surfaceGetThreadId(SurfaceHandle handle);
|
||||
|
||||
@Native<Int Function(SurfaceHandle)>(symbol: 'surface_getGlContext', isLeaf: true)
|
||||
external int surfaceGetGlContext(SurfaceHandle handle);
|
||||
|
||||
@Native<Int32 Function(SurfaceHandle)>(symbol: 'surface_triggerContextLoss', isLeaf: true)
|
||||
external CallbackId surfaceTriggerContextLoss(SurfaceHandle handle);
|
||||
|
||||
@Native<Void Function(SurfaceHandle, OnRenderCallbackHandle)>(
|
||||
symbol: 'surface_setCallbackHandler',
|
||||
isLeaf: true,
|
||||
@ -58,13 +39,15 @@ external void surfaceDestroy(SurfaceHandle surface);
|
||||
)
|
||||
external void surfaceSetResourceCacheLimitBytes(SurfaceHandle surface, int bytes);
|
||||
|
||||
@Native<Int32 Function(SurfaceHandle, Pointer<PictureHandle>, Int)>(
|
||||
@Native<Int32 Function(SurfaceHandle, Pointer<PictureHandle>, Int, Int, Int)>(
|
||||
symbol: 'surface_renderPictures',
|
||||
isLeaf: true,
|
||||
)
|
||||
external CallbackId surfaceRenderPictures(
|
||||
SurfaceHandle surface,
|
||||
Pointer<PictureHandle> picture,
|
||||
int width,
|
||||
int height,
|
||||
int count,
|
||||
);
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ import 'package:ui/ui.dart' as ui;
|
||||
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
|
||||
|
||||
class SkwasmRenderer extends Renderer {
|
||||
late SkwasmSurface surface;
|
||||
|
||||
bool get isMultiThreaded => skwasmIsMultiThreaded();
|
||||
|
||||
bool get isWimp => skwasmIsWimp();
|
||||
@ -319,10 +321,9 @@ class SkwasmRenderer extends Renderer {
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> initialize() async {
|
||||
rasterizer = OffscreenCanvasRasterizer(
|
||||
(OffscreenCanvasProvider canvasProvider) => SkwasmSurface(canvasProvider),
|
||||
);
|
||||
FutureOr<void> initialize() {
|
||||
surface = SkwasmSurface();
|
||||
rasterizer = SkwasmOffscreenCanvasRasterizer(surface);
|
||||
return super.initialize();
|
||||
}
|
||||
|
||||
@ -448,7 +449,7 @@ class SkwasmRenderer extends Renderer {
|
||||
imageSource,
|
||||
imageSource.width,
|
||||
imageSource.height,
|
||||
(pictureToImageSurface as SkwasmSurface).handle,
|
||||
surface.handle,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -469,12 +470,7 @@ class SkwasmRenderer extends Renderer {
|
||||
))).toJSAnyShallow;
|
||||
}
|
||||
return SkwasmImage(
|
||||
imageCreateFromTextureSource(
|
||||
textureSource as JSObject,
|
||||
width,
|
||||
height,
|
||||
(pictureToImageSurface as SkwasmSurface).handle,
|
||||
),
|
||||
imageCreateFromTextureSource(textureSource as JSObject, width, height, surface.handle),
|
||||
);
|
||||
}
|
||||
|
||||
@ -530,11 +526,6 @@ class SkwasmRenderer extends Renderer {
|
||||
|
||||
@override
|
||||
void debugResetRasterizer() {
|
||||
rasterizer = OffscreenCanvasRasterizer(
|
||||
(OffscreenCanvasProvider canvasProvider) => SkwasmSurface(canvasProvider),
|
||||
);
|
||||
rasterizer = SkwasmOffscreenCanvasRasterizer(surface);
|
||||
}
|
||||
|
||||
@override
|
||||
Surface get pictureToImageSurface => (rasterizer as OffscreenCanvasRasterizer).offscreenSurface;
|
||||
}
|
||||
|
||||
@ -45,31 +45,26 @@ class SkwasmCallbackHandler {
|
||||
static SkwasmCallbackHandler instance = SkwasmCallbackHandler._();
|
||||
|
||||
final OnRenderCallbackHandle callbackPointer;
|
||||
final Map<CallbackId, (Completer<JSAny>, Zone)> _pendingCallbacks =
|
||||
<int, (Completer<JSAny>, Zone)>{};
|
||||
final Map<CallbackId, Completer<JSAny>> _pendingCallbacks = <int, Completer<JSAny>>{};
|
||||
|
||||
// Returns a future that will resolve when Skwasm calls back with the given callbackID
|
||||
Future<JSAny> registerCallback(int callbackId) {
|
||||
final completer = Completer<JSAny>();
|
||||
_pendingCallbacks[callbackId] = (completer, Zone.current);
|
||||
_pendingCallbacks[callbackId] = completer;
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void handleCallback(WasmI32 callbackId, WasmI32 context, WasmExternRef? jsContext) {
|
||||
final (Completer<JSAny>, Zone) record = _pendingCallbacks.remove(callbackId.toIntUnsigned())!;
|
||||
final Completer<JSAny> completer = record.$1;
|
||||
final Zone zone = record.$2;
|
||||
zone.run(() {
|
||||
// Skwasm can either callback with a JS object (an externref) or it can call back
|
||||
// with a simple integer, which usually refers to a pointer on its heap. In order
|
||||
// to coerce these into a single type, we just make the completers take a JSAny
|
||||
// that either contains the JS object or a JSNumber that contains the integer value.
|
||||
if (!jsContext.isNull) {
|
||||
completer.complete(jsContext!.toJS);
|
||||
} else {
|
||||
completer.complete(context.toIntUnsigned().toJS);
|
||||
}
|
||||
});
|
||||
// Skwasm can either callback with a JS object (an externref) or it can call back
|
||||
// with a simple integer, which usually refers to a pointer on its heap. In order
|
||||
// to coerce these into a single type, we just make the completers take a JSAny
|
||||
// that either contains the JS object or a JSNumber that contains the integer value.
|
||||
final Completer<JSAny> completer = _pendingCallbacks.remove(callbackId.toIntUnsigned())!;
|
||||
if (!jsContext.isNull) {
|
||||
completer.complete(jsContext!.toJS);
|
||||
} else {
|
||||
completer.complete(context.toIntUnsigned().toJS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,72 +74,51 @@ typedef RenderResult = ({
|
||||
int rasterEndMicros,
|
||||
});
|
||||
|
||||
class SkwasmSurface implements OffscreenSurface {
|
||||
factory SkwasmSurface(OffscreenCanvasProvider canvasProvider) {
|
||||
final SurfaceHandle handle = withStackScope<SurfaceHandle>((StackScope scope) {
|
||||
class SkwasmSurface {
|
||||
factory SkwasmSurface() {
|
||||
final SurfaceHandle surfaceHandle = withStackScope((StackScope scope) {
|
||||
return surfaceCreate();
|
||||
});
|
||||
final surface = SkwasmSurface._fromHandle(handle, canvasProvider);
|
||||
final surface = SkwasmSurface._fromHandle(surfaceHandle);
|
||||
surface._initialize();
|
||||
return surface;
|
||||
}
|
||||
|
||||
SkwasmSurface._fromHandle(this.handle, this._canvasProvider)
|
||||
: _initializedCompleter = Completer<void>() {
|
||||
surfaceSetCallbackHandler(handle, SkwasmCallbackHandler.instance.callbackPointer);
|
||||
_canvas = _canvasProvider.acquireCanvas(const BitmapSize(1, 1), onContextLost: onContextLost);
|
||||
_initialize();
|
||||
}
|
||||
SkwasmSurface._fromHandle(this.handle) : threadId = surfaceGetThreadId(handle);
|
||||
final SurfaceHandle handle;
|
||||
|
||||
final OffscreenCanvasProvider _canvasProvider;
|
||||
late DomOffscreenCanvas _canvas;
|
||||
late SurfaceHandle handle;
|
||||
double _currentDevicePixelRatio = -1;
|
||||
BitmapSize _currentSize = const BitmapSize(1, 1);
|
||||
Completer<void> _initializedCompleter;
|
||||
late Completer<void>? _handledContextLostEvent;
|
||||
|
||||
void onContextLost() {
|
||||
if (!_initializedCompleter.isCompleted) {
|
||||
_initializedCompleter.complete();
|
||||
}
|
||||
_initializedCompleter = Completer<void>();
|
||||
_handledContextLostEvent?.complete();
|
||||
final DomOffscreenCanvas newCanvas = _canvasProvider.acquireCanvas(
|
||||
_currentSize,
|
||||
onContextLost: onContextLost,
|
||||
);
|
||||
recreateContextForCanvas(newCanvas);
|
||||
}
|
||||
final int threadId;
|
||||
|
||||
void _initialize() {
|
||||
final CallbackId callbackId = surfaceSetCanvas(handle, _canvas);
|
||||
|
||||
SkwasmCallbackHandler.instance.registerCallback(callbackId).then((JSAny contextLostCallbackId) {
|
||||
// The context may have been lost before the Surface finished
|
||||
// initializing.
|
||||
if (!_initializedCompleter.isCompleted) {
|
||||
_initializedCompleter.complete();
|
||||
}
|
||||
// Once we have transferred control of the canvas to the Skwasm Surface,
|
||||
// the reference to the _canvas is no longer valid and any listeners
|
||||
// attached to it will never fire. Inform the CanvasProvider that it
|
||||
// should release its reference to the canvas and unregister any listeners
|
||||
// attached to it.
|
||||
_canvasProvider.releaseCanvas(_canvas);
|
||||
SkwasmCallbackHandler.instance
|
||||
.registerCallback((contextLostCallbackId as JSNumber).toDartInt)
|
||||
.then((_) {
|
||||
onContextLost();
|
||||
});
|
||||
});
|
||||
surfaceSetCallbackHandler(handle, SkwasmCallbackHandler.instance.callbackPointer);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ByteData> rasterizeImage(ui.Image image, ui.ImageByteFormat format) async {
|
||||
await initialized;
|
||||
// Cast [image] to [SkwasmImage].
|
||||
image as SkwasmImage;
|
||||
await setSize(BitmapSize(image.width, image.height));
|
||||
Future<RenderResult> renderPictures(List<SkwasmPicture> pictures, int width, int height) =>
|
||||
withStackScope((StackScope scope) async {
|
||||
final Pointer<PictureHandle> pictureHandles = scope
|
||||
.allocPointerArray(pictures.length)
|
||||
.cast<PictureHandle>();
|
||||
for (var i = 0; i < pictures.length; i++) {
|
||||
pictureHandles[i] = pictures[i].handle;
|
||||
}
|
||||
final int callbackId = surfaceRenderPictures(
|
||||
handle,
|
||||
pictureHandles,
|
||||
width,
|
||||
height,
|
||||
pictures.length,
|
||||
);
|
||||
final rasterResult =
|
||||
(await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as RasterResult;
|
||||
final RenderResult result = (
|
||||
imageBitmaps: rasterResult.imageBitmaps.toDart.cast<DomImageBitmap>(),
|
||||
rasterStartMicros: (rasterResult.rasterStartMilliseconds * 1000).toInt(),
|
||||
rasterEndMicros: (rasterResult.rasterEndMilliseconds * 1000).toInt(),
|
||||
);
|
||||
return result;
|
||||
});
|
||||
|
||||
Future<ByteData> rasterizeImage(SkwasmImage image, ui.ImageByteFormat format) async {
|
||||
final int callbackId = surfaceRasterizeImage(handle, image.handle, format.index);
|
||||
final int context =
|
||||
(await SkwasmCallbackHandler.instance.registerCallback(callbackId) as JSNumber).toDartInt;
|
||||
@ -159,88 +133,11 @@ class SkwasmSurface implements OffscreenSurface {
|
||||
return ByteData.sublistView(output);
|
||||
}
|
||||
|
||||
@override
|
||||
void setSkiaResourceCacheMaxBytes(int bytes) {
|
||||
surfaceSetResourceCacheLimitBytes(handle, bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
surfaceDestroy(handle);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DomImageBitmap>> rasterizeToImageBitmaps(List<ui.Picture> pictures) =>
|
||||
withStackScope((StackScope scope) async {
|
||||
await initialized;
|
||||
final Pointer<PictureHandle> pictureHandles = scope
|
||||
.allocPointerArray(pictures.length)
|
||||
.cast<PictureHandle>();
|
||||
for (var i = 0; i < pictures.length; i++) {
|
||||
pictureHandles[i] = (pictures[i] as SkwasmPicture).handle;
|
||||
}
|
||||
final int callbackId = surfaceRenderPictures(handle, pictureHandles, pictures.length);
|
||||
final rasterResult =
|
||||
(await SkwasmCallbackHandler.instance.registerCallback(callbackId)) as RasterResult;
|
||||
final RenderResult result = (
|
||||
imageBitmaps: rasterResult.imageBitmaps.toDart.cast<DomImageBitmap>(),
|
||||
rasterStartMicros: (rasterResult.rasterStartMilliseconds * 1000).toInt(),
|
||||
rasterEndMicros: (rasterResult.rasterEndMilliseconds * 1000).toInt(),
|
||||
);
|
||||
return result.imageBitmaps;
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> recreateContextForCanvas(DomEventTarget newCanvas) async {
|
||||
_canvas = newCanvas as DomOffscreenCanvas;
|
||||
_initialize();
|
||||
await initialized;
|
||||
final BitmapSize lastSize = _currentSize;
|
||||
// Reset _currentSize to force `setSize` to actually size the underlying
|
||||
// Surface.
|
||||
_currentSize = const BitmapSize(1, 1);
|
||||
await setSize(lastSize);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSize(BitmapSize size) async {
|
||||
await initialized;
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
if (_currentSize == size && devicePixelRatio == _currentDevicePixelRatio) {
|
||||
return;
|
||||
}
|
||||
_currentDevicePixelRatio = devicePixelRatio;
|
||||
_currentSize = size;
|
||||
final int callbackId = surfaceSetSize(handle, size.width, size.height);
|
||||
await SkwasmCallbackHandler.instance.registerCallback(callbackId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get glContext => surfaceGetGlContext(handle);
|
||||
|
||||
@override
|
||||
Future<void> get initialized => _initializedCompleter.future;
|
||||
|
||||
@override
|
||||
Future<void> triggerContextLoss() async {
|
||||
_handledContextLostEvent = Completer<void>();
|
||||
final int callbackId = surfaceTriggerContextLoss(handle);
|
||||
await SkwasmCallbackHandler.instance.registerCallback(callbackId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> get handledContextLossEvent => _handledContextLostEvent!.future;
|
||||
|
||||
// TODO(harryterkelsen): Implement this to support MultiSurfaceRasterizer in
|
||||
// Skwasm.
|
||||
@override
|
||||
DomCanvasImageSource get canvasImageSource =>
|
||||
throw StateError('canvasImageSource is not supported for SkwasmSurface');
|
||||
|
||||
// TODO(harryterkelsen): Implement this to support MultiSurfaceRasterizer in
|
||||
// Skwasm.
|
||||
@override
|
||||
Future<void> rasterizeToCanvas(ui.Picture picture) {
|
||||
throw StateError('rasterizeToCanvas is not supported for SkwasmSurface');
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,8 +326,4 @@ class SkwasmRenderer extends Renderer {
|
||||
void debugResetRasterizer() {
|
||||
throw UnimplementedError('Skwasm not implemented on this platform.');
|
||||
}
|
||||
|
||||
@override
|
||||
Surface get pictureToImageSurface =>
|
||||
throw UnimplementedError('Skwasm not implemented on this platform.');
|
||||
}
|
||||
|
||||
@ -1,62 +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';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('Bitmap-less rendering', () {
|
||||
setUpCanvasKitTest(withImplicitView: true);
|
||||
|
||||
setUpAll(() {
|
||||
debugDisableCreateImageBitmapSupport = true;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
debugDisableCreateImageBitmapSupport = false;
|
||||
});
|
||||
|
||||
test(
|
||||
'throws when createImageBitmap is not supported but rasterizeToImageBitmaps is called',
|
||||
() async {
|
||||
final surface = CkOffscreenSurface(OffscreenCanvasProvider());
|
||||
final pictures = <ui.Picture>[];
|
||||
pictures.add(_createPicture());
|
||||
|
||||
expect(() => surface.rasterizeToImageBitmaps(pictures), throwsUnsupportedError);
|
||||
},
|
||||
);
|
||||
|
||||
test('does not throw when rasterizing with a Rasterizer', () async {
|
||||
final builder = ui.SceneBuilder();
|
||||
builder.addPicture(ui.Offset.zero, _createPicture());
|
||||
final ui.Scene scene = builder.build();
|
||||
final LayerTree layerTree = (scene as LayerScene).layerTree;
|
||||
|
||||
final rasterizer = OffscreenCanvasRasterizer(
|
||||
(OffscreenCanvasProvider canvasProvider) => CkOffscreenSurface(canvasProvider),
|
||||
);
|
||||
|
||||
final OffscreenCanvasViewRasterizer viewRasterizer = rasterizer.createViewRasterizer(
|
||||
EnginePlatformDispatcher.instance.implicitView!,
|
||||
);
|
||||
await viewRasterizer.draw(layerTree, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ui.Picture _createPicture() {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
canvas.drawRect(const ui.Rect.fromLTRB(0, 0, 10, 10), ui.Paint());
|
||||
return recorder.endRecording();
|
||||
}
|
||||
@ -65,7 +65,7 @@ void testMain() {
|
||||
|
||||
test('CkImage does not close image source too early', () async {
|
||||
final ImageSource imageSource = ImageBitmapImageSource(
|
||||
await createImageBitmap(createBlankDomImageData(4, 4)),
|
||||
await domWindow.createImageBitmap(createBlankDomImageData(4, 4)),
|
||||
);
|
||||
|
||||
final SkImage skImage1 = canvasKit.MakeAnimatedImageFromEncoded(
|
||||
|
||||
@ -38,14 +38,6 @@ class TestRasterizer extends Rasterizer {
|
||||
List<LayerTree> treesRenderedInView(EngineFlutterView view) {
|
||||
return viewRasterizers[view]!.treesRendered;
|
||||
}
|
||||
|
||||
@override
|
||||
Surface createPictureToImageSurface() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
SurfaceProvider get surfaceProvider => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class TestViewRasterizer extends ViewRasterizer {
|
||||
@ -57,8 +49,8 @@ class TestViewRasterizer extends ViewRasterizer {
|
||||
DisplayCanvasFactory<DisplayCanvas> get displayFactory => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> prepareToDraw() {
|
||||
return Future<void>.value();
|
||||
void prepareToDraw() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
// 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 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
@ -20,85 +23,171 @@ void testMain() {
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(1.0);
|
||||
});
|
||||
|
||||
test('CkOnscreenSurface resizes correctly', () async {
|
||||
final surfaceProvider = OnscreenSurfaceProvider(
|
||||
OnscreenCanvasProvider(),
|
||||
(OnscreenCanvasProvider canvasProvider) => CkOnscreenSurface(canvasProvider),
|
||||
);
|
||||
final surface = surfaceProvider.createSurface() as CkOnscreenSurface;
|
||||
await surface.initialized;
|
||||
final canvas = surface.hostElement.children.single as DomHTMLCanvasElement;
|
||||
ui.Size canvasSize = getCssSize(canvas);
|
||||
|
||||
// Expect size 1x1 initially.
|
||||
expect(canvas.width, 1);
|
||||
expect(canvas.height, 1);
|
||||
expect(canvasSize.width, 1);
|
||||
expect(canvasSize.height, 1);
|
||||
|
||||
await surface.setSize(const BitmapSize(9, 19));
|
||||
canvasSize = getCssSize(canvas);
|
||||
test('Surface allocates canvases efficiently', () {
|
||||
final surface = Surface();
|
||||
surface.createOrUpdateSurface(const BitmapSize(9, 19));
|
||||
final CkSurface originalSurface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas original = surface.debugGetOffscreenCanvas()!;
|
||||
|
||||
// Expect exact requested dimensions.
|
||||
expect(canvas.width, 9);
|
||||
expect(canvas.height, 19);
|
||||
expect(original.width, 9);
|
||||
expect(original.height, 19);
|
||||
expect(originalSurface.width(), 9);
|
||||
expect(originalSurface.height(), 19);
|
||||
|
||||
// Shrinking causes the surface to create a new canvas with the exact
|
||||
// size requested.
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final CkSurface shrunkSurface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas shrunk = surface.debugGetOffscreenCanvas()!;
|
||||
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 to exactly the
|
||||
// requested size.
|
||||
surface.createOrUpdateSurface(const BitmapSize(10, 20));
|
||||
final CkSurface firstIncreaseSurface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas firstIncrease = surface.debugGetOffscreenCanvas()!;
|
||||
expect(firstIncrease, same(original));
|
||||
expect(firstIncreaseSurface, isNot(same(shrunkSurface)));
|
||||
|
||||
// Expect exact dimensions
|
||||
expect(firstIncrease.width, 10);
|
||||
expect(firstIncrease.height, 20);
|
||||
expect(firstIncreaseSurface.width(), 10);
|
||||
expect(firstIncreaseSurface.height(), 20);
|
||||
|
||||
// Subsequent increases within 40% will still allocate a new canvas.
|
||||
surface.createOrUpdateSurface(const BitmapSize(11, 22));
|
||||
final CkSurface secondIncreaseSurface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas secondIncrease = surface.debugGetOffscreenCanvas()!;
|
||||
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.
|
||||
surface.createOrUpdateSurface(const BitmapSize(20, 40));
|
||||
final CkSurface hugeSurface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas huge = surface.debugGetOffscreenCanvas()!;
|
||||
expect(huge, same(secondIncrease));
|
||||
expect(hugeSurface, isNot(same(secondIncreaseSurface)));
|
||||
|
||||
// Also exactly-allocated
|
||||
expect(huge.width, 20);
|
||||
expect(huge.height, 40);
|
||||
expect(hugeSurface.width(), 20);
|
||||
expect(hugeSurface.height(), 40);
|
||||
|
||||
// Shrink again. Create a new surface.
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final CkSurface shrunkSurface2 = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas shrunk2 = surface.debugGetOffscreenCanvas()!;
|
||||
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);
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final CkSurface dpr2Surface = surface.debugGetCkSurface()!;
|
||||
final DomOffscreenCanvas dpr2Canvas = surface.debugGetOffscreenCanvas()!;
|
||||
expect(dpr2Canvas, same(huge));
|
||||
expect(dpr2Surface, isNot(same(hugeSurface)));
|
||||
expect(dpr2Surface.width(), 5);
|
||||
expect(dpr2Surface.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(isDisplayCanvas: true);
|
||||
|
||||
surface.createOrUpdateSurface(const BitmapSize(9, 19));
|
||||
final DomHTMLCanvasElement 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 causes us to resize the canvas.
|
||||
await surface.setSize(const BitmapSize(5, 15));
|
||||
canvasSize = getCssSize(canvas);
|
||||
expect(canvas.width, 5);
|
||||
expect(canvas.height, 15);
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final DomHTMLCanvasElement shrunk = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
expect(shrunk.width, 5);
|
||||
expect(shrunk.height, 15);
|
||||
expect(canvasSize.width, 5);
|
||||
expect(canvasSize.height, 15);
|
||||
|
||||
// Increasing the size causes us to resize the canvas.
|
||||
await surface.setSize(const BitmapSize(10, 20));
|
||||
canvasSize = getCssSize(canvas);
|
||||
surface.createOrUpdateSurface(const BitmapSize(10, 20));
|
||||
final DomHTMLCanvasElement firstIncrease = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
|
||||
expect(firstIncrease, same(original));
|
||||
|
||||
// Expect exact dimensions
|
||||
expect(canvas.width, 10);
|
||||
expect(canvas.height, 20);
|
||||
expect(firstIncrease.width, 10);
|
||||
expect(firstIncrease.height, 20);
|
||||
expect(canvasSize.width, 10);
|
||||
expect(canvasSize.height, 20);
|
||||
|
||||
// Subsequent increases also cause canvas resizing.
|
||||
await surface.setSize(const BitmapSize(11, 22));
|
||||
canvasSize = getCssSize(canvas);
|
||||
surface.createOrUpdateSurface(const BitmapSize(11, 22));
|
||||
final DomHTMLCanvasElement secondIncrease = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
|
||||
expect(canvas.width, 11);
|
||||
expect(canvas.height, 22);
|
||||
expect(secondIncrease, same(firstIncrease));
|
||||
expect(secondIncrease.width, 11);
|
||||
expect(secondIncrease.height, 22);
|
||||
expect(canvasSize.width, 11);
|
||||
expect(canvasSize.height, 22);
|
||||
|
||||
// Increases beyond the 40% limit will cause a canvas resize. STATIC_ASSERT_FOR_WEB
|
||||
await surface.setSize(const BitmapSize(20, 40));
|
||||
canvasSize = getCssSize(canvas);
|
||||
// Increases beyond the 40% limit will cause a canvas resize.
|
||||
surface.createOrUpdateSurface(const BitmapSize(20, 40));
|
||||
final DomHTMLCanvasElement huge = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
|
||||
expect(huge, same(secondIncrease));
|
||||
|
||||
// Also exact
|
||||
expect(canvas.width, 20);
|
||||
expect(canvas.height, 40);
|
||||
expect(huge.width, 20);
|
||||
expect(huge.height, 40);
|
||||
expect(canvasSize.width, 20);
|
||||
expect(canvasSize.height, 40);
|
||||
|
||||
// Shrink again. Resize the canvas.
|
||||
await surface.setSize(const BitmapSize(5, 15));
|
||||
canvasSize = getCssSize(canvas);
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final DomHTMLCanvasElement shrunk2 = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
|
||||
expect(canvas.width, 5);
|
||||
expect(canvas.height, 15);
|
||||
expect(shrunk2, same(huge));
|
||||
expect(shrunk2.width, 5);
|
||||
expect(shrunk2.height, 15);
|
||||
expect(canvasSize.width, 5);
|
||||
expect(canvasSize.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);
|
||||
await surface.setSize(const BitmapSize(5, 15));
|
||||
canvasSize = getCssSize(canvas);
|
||||
surface.createOrUpdateSurface(const BitmapSize(5, 15));
|
||||
final DomHTMLCanvasElement dpr2Canvas = getDisplayCanvas(surface);
|
||||
canvasSize = getCssSize(surface);
|
||||
|
||||
expect(canvas.width, 5);
|
||||
expect(canvas.height, 15);
|
||||
expect(dpr2Canvas, same(huge));
|
||||
expect(dpr2Canvas.width, 5);
|
||||
expect(dpr2Canvas.height, 15);
|
||||
// Canvas is half the size in logical pixels because device pixel ratio is
|
||||
// 2.0.
|
||||
expect(canvasSize.width, 2.5);
|
||||
@ -106,45 +195,160 @@ void testMain() {
|
||||
// Skip on wasm since same() doesn't work for JSValues.
|
||||
}, skip: isWasm);
|
||||
|
||||
test('CkOnscreenSurface falls back to software rendering', () async {
|
||||
CkSurface.debugForceGLFailure = true;
|
||||
final surface = CkOnscreenSurface(OnscreenCanvasProvider());
|
||||
await surface.initialized;
|
||||
test(
|
||||
'Surface creates new context when WebGL context is restored',
|
||||
() async {
|
||||
final surface = Surface();
|
||||
expect(surface.debugForceNewContext, isTrue);
|
||||
surface.createOrUpdateSurface(const BitmapSize(9, 19));
|
||||
final CkSurface before = surface.debugGetCkSurface()!;
|
||||
expect(surface.debugForceNewContext, isFalse);
|
||||
|
||||
expect(surface.supportsWebGl, isFalse);
|
||||
expect(surface.skSurface, isNotNull);
|
||||
CkSurface.debugForceGLFailure = false;
|
||||
});
|
||||
// Pump a timer to flush any microtasks.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
surface.createOrUpdateSurface(const BitmapSize(9, 19));
|
||||
final CkSurface afterAcquireFrame = surface.debugGetCkSurface()!;
|
||||
// Existing context is reused.
|
||||
expect(afterAcquireFrame, same(before));
|
||||
|
||||
test('CkOffscreenSurface falls back to software rendering', () async {
|
||||
CkSurface.debugForceGLFailure = true;
|
||||
final surface = CkOffscreenSurface(OffscreenCanvasProvider());
|
||||
await surface.initialized;
|
||||
// Emulate WebGL context loss.
|
||||
final DomOffscreenCanvas canvas = surface.debugGetOffscreenCanvas()!;
|
||||
final WebGLContext ctx = canvas.getGlContext(2);
|
||||
final WebGLLoseContextExtension loseContextExtension = ctx.loseContextExtension;
|
||||
loseContextExtension.loseContext();
|
||||
|
||||
expect(surface.supportsWebGl, isFalse);
|
||||
expect(surface.skSurface, isNotNull);
|
||||
CkSurface.debugForceGLFailure = false;
|
||||
});
|
||||
// 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 = ctx.isContextLost();
|
||||
expect(isContextLost, isTrue);
|
||||
|
||||
test('does not recreate surface if size is the same', () async {
|
||||
final surfaceProvider = OnscreenSurfaceProvider(
|
||||
OnscreenCanvasProvider(),
|
||||
(OnscreenCanvasProvider canvasProvider) => CkOnscreenSurface(canvasProvider),
|
||||
// Emulate WebGL context restoration.
|
||||
loseContextExtension.restoreContext();
|
||||
|
||||
// Pump a timer to allow the "restore context" event to propagate.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(surface.debugForceNewContext, isTrue);
|
||||
|
||||
surface.createOrUpdateSurface(const BitmapSize(9, 19));
|
||||
final CkSurface afterContextLost = surface.debugGetCkSurface()!;
|
||||
// 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.createOrUpdateSurface(const BitmapSize(10, 16));
|
||||
final CkSurface original = surface.debugGetCkSurface()!;
|
||||
|
||||
expect(original.width(), 10);
|
||||
expect(original.height(), 16);
|
||||
expect(surface.debugGetOffscreenCanvas()!.width, 10);
|
||||
expect(surface.debugGetOffscreenCanvas()!.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);
|
||||
surface.createOrUpdateSurface(const BitmapSize(10, 16));
|
||||
final CkSurface highDpr = surface.debugGetCkSurface()!;
|
||||
expect(highDpr.width(), 10);
|
||||
expect(highDpr.height(), 16);
|
||||
expect(surface.debugGetOffscreenCanvas()!.width, 10);
|
||||
expect(surface.debugGetOffscreenCanvas()!.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);
|
||||
surface.createOrUpdateSurface(const BitmapSize(10, 16));
|
||||
final CkSurface lowDpr = surface.debugGetCkSurface()!;
|
||||
expect(lowDpr.width(), 10);
|
||||
expect(lowDpr.height(), 16);
|
||||
expect(surface.debugGetOffscreenCanvas()!.width, 10);
|
||||
expect(surface.debugGetOffscreenCanvas()!.height, 16);
|
||||
|
||||
// See https://github.com/flutter/flutter/issues/77084#issuecomment-1120151172
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.0);
|
||||
surface.createOrUpdateSurface(BitmapSize.fromSize(const ui.Size(9.9, 15.9)));
|
||||
final CkSurface changeRatioAndSize = surface.debugGetCkSurface()!;
|
||||
expect(changeRatioAndSize.width(), 10);
|
||||
expect(changeRatioAndSize.height(), 16);
|
||||
expect(surface.debugGetOffscreenCanvas()!.width, 10);
|
||||
expect(surface.debugGetOffscreenCanvas()!.height, 16);
|
||||
},
|
||||
skip: !Surface.offscreenCanvasSupported,
|
||||
);
|
||||
|
||||
test('uses transferToImageBitmap for bitmap creation', () async {
|
||||
final surface = Surface();
|
||||
surface.ensureSurface(const BitmapSize(10, 10));
|
||||
final DomOffscreenCanvas offscreenCanvas = surface.debugGetOffscreenCanvas()!;
|
||||
final transferToImageBitmap = offscreenCanvas['transferToImageBitmap']! as JSFunction;
|
||||
var transferToImageBitmapCalls = 0;
|
||||
offscreenCanvas['transferToImageBitmap'] = () {
|
||||
transferToImageBitmapCalls++;
|
||||
return transferToImageBitmap.callAsFunction(offscreenCanvas);
|
||||
}.toJS;
|
||||
final renderCanvas = RenderCanvas();
|
||||
final recorder = CkPictureRecorder();
|
||||
final CkCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
canvas.drawCircle(
|
||||
const ui.Offset(5, 5),
|
||||
3,
|
||||
CkPaint()..color = const ui.Color.fromARGB(255, 255, 0, 0),
|
||||
);
|
||||
final surface = surfaceProvider.createSurface() as CkOnscreenSurface;
|
||||
await surface.initialized;
|
||||
await surface.setSize(const BitmapSize(10, 20));
|
||||
final SkSurface? skSurface1 = surface.skSurface;
|
||||
await surface.setSize(const BitmapSize(10, 20));
|
||||
final SkSurface? skSurface2 = surface.skSurface;
|
||||
expect(skSurface1, same(skSurface2));
|
||||
final CkPicture picture = recorder.endRecording();
|
||||
await surface.rasterizeToCanvas(const BitmapSize(10, 10), renderCanvas, picture);
|
||||
expect(transferToImageBitmapCalls, 1);
|
||||
}, skip: !Surface.offscreenCanvasSupported);
|
||||
|
||||
test('throws error if CanvasKit.MakeGrContext returns null', () async {
|
||||
canvasKit['MakeGrContext'] = ((int glContext) => null).toJS;
|
||||
final surface = Surface();
|
||||
expect(() => surface.ensureSurface(const BitmapSize(10, 10)), throwsA(isA<CanvasKitError>()));
|
||||
// Skipping on Firefox for now since Firefox headless doesn't support WebGL
|
||||
}, skip: isFirefox);
|
||||
|
||||
test('can recover from MakeSWCanvasSurface failure', () async {
|
||||
debugOverrideJsConfiguration(
|
||||
<String, Object?>{'canvasKitForceCpuOnly': true}.jsify() as JsFlutterConfiguration?,
|
||||
);
|
||||
addTearDown(() => debugOverrideJsConfiguration(null));
|
||||
|
||||
final surface = Surface();
|
||||
surface.debugThrowOnSoftwareSurfaceCreation = true;
|
||||
expect(
|
||||
() => surface.createOrUpdateSurface(const BitmapSize(12, 34)),
|
||||
throwsA(isA<CanvasKitError>()),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(surface.debugForceNewContext, isFalse);
|
||||
|
||||
surface.debugThrowOnSoftwareSurfaceCreation = false;
|
||||
final CkSurface ckSurface = surface.createOrUpdateSurface(const BitmapSize(12, 34));
|
||||
|
||||
expect(ckSurface.surface.width(), 12);
|
||||
expect(ckSurface.surface.height(), 34);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
DomHTMLCanvasElement getDisplayCanvas(Surface surface) {
|
||||
assert(surface.isDisplayCanvas);
|
||||
return surface.hostElement.children.first as DomHTMLCanvasElement;
|
||||
}
|
||||
|
||||
/// Extracts the CSS style values of 'width' and 'height' and returns them
|
||||
/// as a [ui.Size].
|
||||
ui.Size getCssSize(DomHTMLCanvasElement canvas) {
|
||||
ui.Size getCssSize(Surface surface) {
|
||||
final DomHTMLCanvasElement 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
|
||||
|
||||
@ -31,7 +31,6 @@ void setUpUnitTests({
|
||||
<String, Object?>{'fontFallbackBaseUrl': 'assets/fallback_fonts/'}.jsify()
|
||||
as engine.JsFlutterConfiguration?,
|
||||
);
|
||||
engine.debugThrowOnCreateImageBitmapIfDisabled = true;
|
||||
|
||||
if (setUpTestViewDimensions) {
|
||||
// The following parameters are hard-coded in Flutter's test embedder. Since
|
||||
|
||||
@ -35,14 +35,6 @@ class TestRasterizer extends Rasterizer {
|
||||
List<LayerTree> treesRenderedInView(EngineFlutterView view) {
|
||||
return viewRasterizers[view]!.treesRendered;
|
||||
}
|
||||
|
||||
@override
|
||||
Surface createPictureToImageSurface() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
SurfaceProvider get surfaceProvider => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class TestViewRasterizer extends ViewRasterizer {
|
||||
@ -54,8 +46,8 @@ class TestViewRasterizer extends ViewRasterizer {
|
||||
DisplayCanvasFactory<DisplayCanvas> get displayFactory => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> prepareToDraw() {
|
||||
return Future<void>.value();
|
||||
void prepareToDraw() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -178,7 +178,7 @@ class FakeRasterizer extends ViewRasterizer {
|
||||
DisplayCanvasFactory<DisplayCanvas> get displayFactory => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> prepareToDraw() {
|
||||
void prepareToDraw() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
@ -1,134 +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:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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/test_initialization.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('OffscreenSurface', () {
|
||||
setUpUnitTests();
|
||||
|
||||
test(
|
||||
'creates new context when WebGL context is lost',
|
||||
() async {
|
||||
final Rasterizer rasterizer = renderer.rasterizer;
|
||||
final surfaceProvider = rasterizer.surfaceProvider as OffscreenSurfaceProvider;
|
||||
final OffscreenSurface surface = surfaceProvider.createSurface();
|
||||
await surface.initialized;
|
||||
|
||||
final int initialGlContext = surface.glContext;
|
||||
|
||||
await surface.triggerContextLoss();
|
||||
await surface.handledContextLossEvent;
|
||||
await surface.initialized;
|
||||
|
||||
// A new context is created.
|
||||
expect(surface.glContext, isNot(initialGlContext));
|
||||
},
|
||||
skip: isFirefox || isSafari || !browserSupportsOffscreenCanvas,
|
||||
);
|
||||
|
||||
test(
|
||||
'can still render after context is lost',
|
||||
() async {
|
||||
final Rasterizer rasterizer = renderer.rasterizer;
|
||||
final surfaceProvider = rasterizer.surfaceProvider as OffscreenSurfaceProvider;
|
||||
final OffscreenSurface surface = surfaceProvider.createSurface();
|
||||
await surface.initialized;
|
||||
|
||||
await surface.setSize(const BitmapSize(10, 10));
|
||||
|
||||
// Draw a red square.
|
||||
final ui.Picture redPicture = drawPicture((ui.Canvas canvas) {
|
||||
canvas.drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 10, 10),
|
||||
ui.Paint()..color = const ui.Color(0xFFFF0000),
|
||||
);
|
||||
});
|
||||
List<DomImageBitmap> bitmaps = await surface.rasterizeToImageBitmaps(<ui.Picture>[
|
||||
redPicture,
|
||||
]);
|
||||
expect(bitmaps, hasLength(1));
|
||||
await expectBitmapColor(bitmaps.single, const ui.Color(0xFFFF0000));
|
||||
|
||||
// Lose the context.
|
||||
await surface.triggerContextLoss();
|
||||
await surface.handledContextLossEvent;
|
||||
await surface.initialized;
|
||||
|
||||
// Draw a blue square.
|
||||
final ui.Picture bluePicture = drawPicture((ui.Canvas canvas) {
|
||||
canvas.drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 10, 10),
|
||||
ui.Paint()..color = const ui.Color(0xFF0000FF),
|
||||
);
|
||||
});
|
||||
bitmaps = await surface.rasterizeToImageBitmaps(<ui.Picture>[bluePicture]);
|
||||
expect(bitmaps, hasLength(1));
|
||||
await expectBitmapColor(bitmaps.single, const ui.Color(0xFF0000FF));
|
||||
},
|
||||
skip: isFirefox || isSafari || !browserSupportsOffscreenCanvas,
|
||||
);
|
||||
|
||||
test(
|
||||
'can recover from multiple context losses',
|
||||
() async {
|
||||
final Rasterizer rasterizer = renderer.rasterizer;
|
||||
final surfaceProvider = rasterizer.surfaceProvider as OffscreenSurfaceProvider;
|
||||
final OffscreenSurface surface = surfaceProvider.createSurface();
|
||||
await surface.initialized;
|
||||
|
||||
final int initialGlContext = surface.glContext;
|
||||
|
||||
// First loss
|
||||
await surface.triggerContextLoss();
|
||||
await surface.handledContextLossEvent;
|
||||
await surface.initialized;
|
||||
final int contextAfterFirstLoss = surface.glContext;
|
||||
expect(contextAfterFirstLoss, isNot(initialGlContext));
|
||||
|
||||
// Second loss
|
||||
await surface.triggerContextLoss();
|
||||
await surface.handledContextLossEvent;
|
||||
await surface.initialized;
|
||||
final int contextAfterSecondLoss = surface.glContext;
|
||||
expect(contextAfterSecondLoss, isNot(contextAfterFirstLoss));
|
||||
},
|
||||
skip: isFirefox || isSafari || !browserSupportsOffscreenCanvas,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ui.Picture drawPicture(void Function(ui.Canvas) drawCommands) {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(recorder);
|
||||
drawCommands(canvas);
|
||||
return recorder.endRecording();
|
||||
}
|
||||
|
||||
Future<void> expectBitmapColor(DomImageBitmap bitmap, ui.Color color) async {
|
||||
final DomHTMLCanvasElement canvas = createDomCanvasElement(
|
||||
width: bitmap.width,
|
||||
height: bitmap.height,
|
||||
);
|
||||
final DomCanvasRenderingContext2D ctx = canvas.context2D;
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
final DomImageData imageData = ctx.getImageData(0, 0, 1, 1);
|
||||
final Uint8ClampedList pixels = imageData.data;
|
||||
expect(pixels[0], color.red);
|
||||
expect(pixels[1], color.green);
|
||||
expect(pixels[2], color.blue);
|
||||
expect(pixels[3], color.alpha);
|
||||
}
|
||||
@ -72,10 +72,18 @@ mergeInto(LibraryManager.library, {
|
||||
};
|
||||
}
|
||||
|
||||
const handleToContextLostHandlerMap = new Map();
|
||||
const handleToCanvasMap = new Map();
|
||||
const associatedObjectsMap = new Map();
|
||||
|
||||
_skwasm_setAssociatedObjectOnThread = function(threadId, pointer, object) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'setAssociatedObject',
|
||||
pointer,
|
||||
object,
|
||||
}, [object], threadId);
|
||||
};
|
||||
_skwasm_getAssociatedObject = function(pointer) {
|
||||
return associatedObjectsMap.get(pointer);
|
||||
};
|
||||
_skwasm_connectThread = function(threadId) {
|
||||
const eventListener = function(data) {
|
||||
const skwasmMessage = data.skwasmMessage;
|
||||
@ -83,35 +91,12 @@ mergeInto(LibraryManager.library, {
|
||||
return;
|
||||
}
|
||||
switch (skwasmMessage) {
|
||||
case 'transferCanvas':
|
||||
_surface_receiveCanvasOnWorker(
|
||||
data.surface,
|
||||
data.canvas,
|
||||
data.callbackId,
|
||||
);
|
||||
return;
|
||||
case 'onInitialized':
|
||||
_surface_onInitialized(data.surface, data.callbackId);
|
||||
return;
|
||||
case 'resizeSurface':
|
||||
_surface_resizeOnWorker(data.surface, data.width, data.height, data.callbackId);
|
||||
return;
|
||||
case 'onResizeComplete':
|
||||
_surface_onResizeComplete(data.surface, data.callbackId);
|
||||
return;
|
||||
case 'triggerContextLoss':
|
||||
_surface_triggerContextLossOnWorker(data.surface, data.callbackId);
|
||||
return;
|
||||
case 'onContextLossTriggered':
|
||||
_surface_onContextLossTriggered(data.surface, data.callbackId);
|
||||
return;
|
||||
case 'reportContextLost':
|
||||
_surface_reportContextLost(data.surface, data.callbackId);
|
||||
return;
|
||||
case 'renderPictures':
|
||||
_surface_renderPicturesOnWorker(
|
||||
data.surface,
|
||||
data.pictures,
|
||||
data.width,
|
||||
data.height,
|
||||
data.pictureCount,
|
||||
data.callbackId,
|
||||
skwasm_getCurrentTimestamp());
|
||||
@ -161,83 +146,45 @@ mergeInto(LibraryManager.library, {
|
||||
};
|
||||
skwasm_registerMessageListener(threadId, eventListener);
|
||||
};
|
||||
|
||||
// Associated Objects
|
||||
_skwasm_setAssociatedObjectOnThread = function(threadId, pointer, object) {
|
||||
_skwasm_dispatchRenderPictures = function (threadId, surfaceHandle, pictures, width, height, pictureCount, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'setAssociatedObject',
|
||||
pointer,
|
||||
object,
|
||||
}, [object], threadId);
|
||||
};
|
||||
_skwasm_getAssociatedObject = function(pointer) {
|
||||
return associatedObjectsMap.get(pointer);
|
||||
};
|
||||
_skwasm_disposeAssociatedObjectOnThread = function(threadId, pointer) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'disposeAssociatedObject',
|
||||
pointer,
|
||||
}, [], threadId);
|
||||
};
|
||||
|
||||
// Surface Lifecycle
|
||||
_skwasm_dispatchDisposeSurface = function(threadId, surface) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'disposeSurface',
|
||||
surface,
|
||||
}, [], threadId);
|
||||
}
|
||||
|
||||
// Surface Setup
|
||||
_skwasm_dispatchTransferCanvas = function (threadId, surfaceHandle, canvas, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'transferCanvas',
|
||||
skwasmMessage: 'renderPictures',
|
||||
surface: surfaceHandle,
|
||||
canvas,
|
||||
callbackId,
|
||||
}, [canvas], threadId);
|
||||
};
|
||||
_skwasm_reportInitialized = function (surfaceHandle, contextLostCallbackId, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'onInitialized',
|
||||
surface: surfaceHandle,
|
||||
contextLostCallbackId,
|
||||
callbackId,
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Resizing
|
||||
_skwasm_dispatchResizeSurface = function (threadId, surface, width, height, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'resizeSurface',
|
||||
surface,
|
||||
pictures,
|
||||
width,
|
||||
height,
|
||||
pictureCount,
|
||||
callbackId,
|
||||
}, [], threadId);
|
||||
}
|
||||
_skwasm_reportResizeComplete = function (surfaceHandle, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'onResizeComplete',
|
||||
surface: surfaceHandle,
|
||||
callbackId,
|
||||
}, []);
|
||||
};
|
||||
_skwasm_createOffscreenCanvas = function(width, height) {
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
var contextAttributes = {
|
||||
majorVersion: 2,
|
||||
alpha: true,
|
||||
depth: true,
|
||||
stencil: true,
|
||||
antialias: false,
|
||||
premultipliedAlpha: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: 'default',
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
enableExtensionsByDefault: true,
|
||||
};
|
||||
const contextHandle = GL.createContext(canvas, contextAttributes);
|
||||
handleToCanvasMap.set(contextHandle, canvas);
|
||||
return contextHandle;
|
||||
};
|
||||
_skwasm_resizeCanvas = function(contextHandle, width, height) {
|
||||
const canvas = handleToCanvasMap.get(contextHandle);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
};
|
||||
|
||||
// Rendering
|
||||
_skwasm_dispatchRenderPictures = function (threadId, surfaceHandle, pictures, pictureCount, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'renderPictures',
|
||||
surface: surfaceHandle,
|
||||
pictures,
|
||||
pictureCount,
|
||||
callbackId,
|
||||
}, [], threadId);
|
||||
_skwasm_captureImageBitmap = function (contextHandle, imageBitmaps) {
|
||||
if (!imageBitmaps) imageBitmaps = Array();
|
||||
const canvas = handleToCanvasMap.get(contextHandle);
|
||||
imageBitmaps.push(canvas.transferToImageBitmap());
|
||||
return imageBitmaps;
|
||||
};
|
||||
_skwasm_resolveAndPostImages = async function (surfaceHandle, imageBitmaps, rasterStart, callbackId) {
|
||||
if (!imageBitmaps) imageBitmaps = Array();
|
||||
@ -251,14 +198,33 @@ mergeInto(LibraryManager.library, {
|
||||
rasterEnd,
|
||||
}, [...imageBitmaps]);
|
||||
};
|
||||
_skwasm_captureImageBitmap = function (contextHandle, imageBitmaps) {
|
||||
if (!imageBitmaps) imageBitmaps = Array();
|
||||
const canvas = handleToCanvasMap.get(contextHandle);
|
||||
imageBitmaps.push(canvas.transferToImageBitmap());
|
||||
return imageBitmaps;
|
||||
};
|
||||
_skwasm_createGlTextureFromTextureSource = function(textureSource, width, height) {
|
||||
const glCtx = GL.currentContext.GLctx;
|
||||
const newTexture = glCtx.createTexture();
|
||||
glCtx.bindTexture(glCtx.TEXTURE_2D, newTexture);
|
||||
glCtx.pixelStorei(glCtx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
||||
|
||||
// Image Rasterization
|
||||
glCtx.texImage2D(glCtx.TEXTURE_2D, 0, glCtx.RGBA, width, height, 0, glCtx.RGBA, glCtx.UNSIGNED_BYTE, textureSource);
|
||||
|
||||
glCtx.pixelStorei(glCtx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
||||
glCtx.bindTexture(glCtx.TEXTURE_2D, null);
|
||||
|
||||
const textureId = GL.getNewId(GL.textures);
|
||||
GL.textures[textureId] = newTexture;
|
||||
return textureId;
|
||||
};
|
||||
_skwasm_disposeAssociatedObjectOnThread = function(threadId, pointer) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'disposeAssociatedObject',
|
||||
pointer,
|
||||
}, [], threadId);
|
||||
};
|
||||
_skwasm_dispatchDisposeSurface = function(threadId, surface) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'disposeSurface',
|
||||
surface,
|
||||
}, [], threadId);
|
||||
}
|
||||
_skwasm_dispatchRasterizeImage = function(threadId, surface, image, format, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'rasterizeImage',
|
||||
@ -276,89 +242,6 @@ mergeInto(LibraryManager.library, {
|
||||
callbackId,
|
||||
});
|
||||
}
|
||||
|
||||
// Context Loss
|
||||
_skwasm_dispatchTriggerContextLoss = function (threadId, surfaceHandle, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'triggerContextLoss',
|
||||
surface: surfaceHandle,
|
||||
callbackId,
|
||||
}, [], threadId);
|
||||
};
|
||||
_skwasm_reportContextLossTriggered = function (surfaceHandle, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'onContextLossTriggered',
|
||||
surface: surfaceHandle,
|
||||
callbackId,
|
||||
}, []);
|
||||
};
|
||||
_skwasm_reportContextLost = function (surfaceHandle, callbackId) {
|
||||
skwasm_postMessage({
|
||||
skwasmMessage: 'reportContextLost',
|
||||
surface: surfaceHandle,
|
||||
callbackId,
|
||||
}, []);
|
||||
};
|
||||
_skwasm_triggerContextLossOnCanvas = function () {
|
||||
const glCtx = GL.currentContext.GLctx;
|
||||
glCtx.getExtension("WEBGL_lose_context").loseContext();
|
||||
};
|
||||
|
||||
// GL Context
|
||||
_skwasm_getGlContextForCanvas = function (canvas, surfaceHandle) {
|
||||
var contextAttributes = {
|
||||
majorVersion: 2,
|
||||
alpha: true,
|
||||
depth: true,
|
||||
stencil: true,
|
||||
antialias: false,
|
||||
premultipliedAlpha: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: 'default',
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
enableExtensionsByDefault: true,
|
||||
};
|
||||
const contextHandle = GL.createContext(canvas, contextAttributes);
|
||||
handleToCanvasMap.set(contextHandle, canvas);
|
||||
|
||||
// Register an event listener for the context lost event.
|
||||
var contextLostHandler;
|
||||
contextLostHandler = function (e) {
|
||||
e.preventDefault();
|
||||
_surface_onContextLost(surfaceHandle);
|
||||
canvas.removeEventListener('webglcontextlost', contextLostHandler);
|
||||
}
|
||||
canvas.addEventListener('webglcontextlost', contextLostHandler);
|
||||
handleToContextLostHandlerMap.set(contextHandle, contextLostHandler);
|
||||
return contextHandle;
|
||||
};
|
||||
_skwasm_destroyContext = function (contextHandle) {
|
||||
const canvas = handleToCanvasMap.get(contextHandle);
|
||||
const handler = handleToContextLostHandlerMap.get(contextHandle);
|
||||
if (canvas && handler) {
|
||||
canvas.removeEventListener('webglcontextlost', handler);
|
||||
}
|
||||
GL.deleteContext(contextHandle);
|
||||
handleToCanvasMap.delete(contextHandle);
|
||||
handleToContextLostHandlerMap.delete(contextHandle);
|
||||
};
|
||||
|
||||
// Texture Sources
|
||||
_skwasm_createGlTextureFromTextureSource = function(textureSource, width, height) {
|
||||
const glCtx = GL.currentContext.GLctx;
|
||||
const newTexture = glCtx.createTexture();
|
||||
glCtx.bindTexture(glCtx.TEXTURE_2D, newTexture);
|
||||
glCtx.pixelStorei(glCtx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
||||
|
||||
glCtx.texImage2D(glCtx.TEXTURE_2D, 0, glCtx.RGBA, width, height, 0, glCtx.RGBA, glCtx.UNSIGNED_BYTE, textureSource);
|
||||
|
||||
glCtx.pixelStorei(glCtx.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
||||
glCtx.bindTexture(glCtx.TEXTURE_2D, null);
|
||||
|
||||
const textureId = GL.getNewId(GL.textures);
|
||||
GL.textures[textureId] = newTexture;
|
||||
return textureId;
|
||||
};
|
||||
},
|
||||
$skwasm_registerMessageListener: function() {},
|
||||
$skwasm_registerMessageListener__deps: ['$skwasm_support_setup'],
|
||||
@ -376,28 +259,10 @@ mergeInto(LibraryManager.library, {
|
||||
skwasm_disposeAssociatedObjectOnThread__deps: ['$skwasm_support_setup'],
|
||||
skwasm_connectThread: function() {},
|
||||
skwasm_connectThread__deps: ['$skwasm_support_setup', '$skwasm_registerMessageListener', '$skwasm_getCurrentTimestamp'],
|
||||
skwasm_dispatchTransferCanvas: function () { },
|
||||
skwasm_dispatchTransferCanvas__deps: ['$skwasm_support_setup'],
|
||||
skwasm_reportInitialized: function () { },
|
||||
skwasm_reportInitialized__deps: ['$skwasm_support_setup'],
|
||||
skwasm_reportResizeComplete: function () { },
|
||||
skwasm_reportResizeComplete__deps: ['$skwasm_support_setup'],
|
||||
skwasm_getGlContextForCanvas: function () { },
|
||||
skwasm_getGlContextForCanvas__deps: ['$skwasm_support_setup'],
|
||||
skwasm_dispatchTriggerContextLoss: function () { },
|
||||
skwasm_dispatchTriggerContextLoss__deps: ['$skwasm_support_setup'],
|
||||
skwasm_triggerContextLossOnCanvas: function () { },
|
||||
skwasm_triggerContextLossOnCanvas__deps: ['$skwasm_support_setup'],
|
||||
skwasm_reportContextLossTriggered: function () { },
|
||||
skwasm_reportContextLossTriggered__deps: ['$skwasm_support_setup'],
|
||||
skwasm_reportContextLost: function () { },
|
||||
skwasm_reportContextLost__deps: ['$skwasm_support_setup'],
|
||||
skwasm_destroyContext: function () { },
|
||||
skwasm_destroyContext__deps: ['$skwasm_support_setup'],
|
||||
skwasm_dispatchResizeSurface: function () { },
|
||||
skwasm_dispatchResizeSurface__deps: ['$skwasm_support_setup'],
|
||||
skwasm_dispatchRenderPictures: function() {},
|
||||
skwasm_dispatchRenderPictures__deps: ['$skwasm_support_setup'],
|
||||
skwasm_createOffscreenCanvas: function () {},
|
||||
skwasm_createOffscreenCanvas__deps: ['$skwasm_support_setup'],
|
||||
skwasm_resizeCanvas: function () {},
|
||||
skwasm_resizeCanvas__deps: ['$skwasm_support_setup'],
|
||||
skwasm_captureImageBitmap: function () {},
|
||||
|
||||
@ -32,21 +32,12 @@ extern void skwasm_connectThread(pthread_t thread_id);
|
||||
extern void skwasm_dispatchRenderPictures(unsigned long thread_id,
|
||||
Skwasm::Surface* surface,
|
||||
sk_sp<flutter::DisplayList>* pictures,
|
||||
int width,
|
||||
int height,
|
||||
int count,
|
||||
uint32_t callback_id);
|
||||
extern uint32_t skwasm_getGlContextForCanvas(SkwasmObject canvas,
|
||||
Skwasm::Surface* surface);
|
||||
extern void skwasm_reportInitialized(Skwasm::Surface* surface,
|
||||
uint32_t callback_id,
|
||||
uint32_t context_lost_callback_id);
|
||||
extern void skwasm_reportResizeComplete(Skwasm::Surface* surface,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_dispatchResizeSurface(unsigned long thread_id,
|
||||
Skwasm::Surface* surface,
|
||||
int width,
|
||||
int height,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_resizeCanvas(uint32_t contextHandle, int width, int height);
|
||||
extern uint32_t skwasm_createOffscreenCanvas(int width, int height);
|
||||
extern void skwasm_resizeCanvas(uint32_t context_handle, int width, int height);
|
||||
extern SkwasmObject skwasm_captureImageBitmap(uint32_t context_handle,
|
||||
SkwasmObject image_bitmaps);
|
||||
extern void skwasm_resolveAndPostImages(Skwasm::Surface* surface,
|
||||
@ -57,19 +48,6 @@ extern unsigned int skwasm_createGlTextureFromTextureSource(
|
||||
SkwasmObject texture_source,
|
||||
int width,
|
||||
int height);
|
||||
extern void skwasm_dispatchTriggerContextLoss(unsigned long thread_id,
|
||||
Skwasm::Surface* surface,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_triggerContextLossOnCanvas();
|
||||
extern void skwasm_reportContextLossTriggered(Skwasm::Surface* surface,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_reportContextLost(Skwasm::Surface* surface,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_destroyContext(uint32_t context_handle);
|
||||
extern void skwasm_dispatchTransferCanvas(unsigned long thread_id,
|
||||
Skwasm::Surface* surface,
|
||||
SkwasmObject canvas,
|
||||
uint32_t callback_id);
|
||||
extern void skwasm_dispatchDisposeSurface(unsigned long thread_id,
|
||||
Skwasm::Surface* surface);
|
||||
extern void skwasm_dispatchRasterizeImage(unsigned long thread_id,
|
||||
|
||||
@ -14,33 +14,6 @@
|
||||
#include "flutter/skwasm/skwasm_support.h"
|
||||
#include "third_party/skia/include/core/SkColorSpace.h"
|
||||
|
||||
// This file implements the C++ side of the Skwasm Surface API.
|
||||
//
|
||||
// The general lifecycle of a method call that needs to be performed on the
|
||||
// web worker thread is as follows:
|
||||
//
|
||||
// 1. The method is called on the [Surface] object on the main thread.
|
||||
// This method will have the same name as the dart method that is calling it.
|
||||
// It will extract the arguments, generate a callback id, and then call a
|
||||
// `skwasm_dispatch*` method to send a message to the worker thread.
|
||||
// 2. The `skwasm_dispatch*` method will be a javascript method in
|
||||
// `library_skwasm_support.js`. This method will use `postMessage` to send a
|
||||
// message to the worker thread.
|
||||
// 3. The worker thread will receive the message in its `message` event
|
||||
// listener. The listener will call a `surface_*OnWorker` C++ method.
|
||||
// 4. The `surface_*OnWorker` method will call the corresponding `*OnWorker`
|
||||
// method on the [Surface] object. This method will do the actual work of
|
||||
// the method call.
|
||||
// 5. When the work is complete, the `*OnWorker` method will call a
|
||||
// `skwasm_report*` method. This will be a javascript method in
|
||||
// `library_skwasm_support.js` which will use `postMessage` to send a
|
||||
// message back to the main thread.
|
||||
// 6. The main thread will receive the message in its `message` event listener.
|
||||
// The listener will call an `on*` method on the C++ [Surface] object.
|
||||
// 7. The `on*` method will invoke the callback handler that was registered by
|
||||
// the Dart code, which will complete the future that was returned by the
|
||||
// original Dart method call.
|
||||
|
||||
Skwasm::Surface::Surface() {
|
||||
if (skwasm_isSingleThreaded()) {
|
||||
skwasm_connectThread(0);
|
||||
@ -58,45 +31,66 @@ Skwasm::Surface::Surface() {
|
||||
}
|
||||
}
|
||||
|
||||
// General getters are implemented in the header.
|
||||
|
||||
// Lifecycle
|
||||
|
||||
void Skwasm::Surface::SetCallbackHandler(CallbackHandler* callback_handler) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_ = callback_handler;
|
||||
}
|
||||
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::Dispose() {
|
||||
if (gl_context_) {
|
||||
skwasm_destroyContext(gl_context_);
|
||||
}
|
||||
delete this;
|
||||
}
|
||||
|
||||
// Main thread only
|
||||
uint32_t Skwasm::Surface::SetCanvas(SkwasmObject canvas) {
|
||||
void Skwasm::Surface::SetResourceCacheLimit(int bytes) {
|
||||
render_context_->SetResourceCacheLimit(bytes);
|
||||
}
|
||||
|
||||
// Main thread only
|
||||
uint32_t Skwasm::Surface::RenderPictures(flutter::DisplayList** pictures,
|
||||
int width,
|
||||
int height,
|
||||
int count) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
skwasm_dispatchTransferCanvas(thread_, this, canvas, callback_id);
|
||||
std::unique_ptr<sk_sp<flutter::DisplayList>[]> picture_pointers =
|
||||
std::make_unique<sk_sp<flutter::DisplayList>[]>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
picture_pointers[i] = sk_ref_sp(pictures[i]);
|
||||
}
|
||||
|
||||
// Releasing picturePointers here and will recreate the unique_ptr on the
|
||||
// other thread See surface_renderPicturesOnWorker
|
||||
skwasm_dispatchRenderPictures(thread_, this, picture_pointers.release(),
|
||||
width, height, count, callback_id);
|
||||
return callback_id;
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnInitialized(uint32_t callback_id) {
|
||||
// Main thread only
|
||||
uint32_t Skwasm::Surface::RasterizeImage(flutter::DlImage* image,
|
||||
Skwasm::ImageByteFormat format) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, (void*)context_lost_callback_id_,
|
||||
__builtin_wasm_ref_null_extern());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
image->ref();
|
||||
|
||||
skwasm_dispatchRasterizeImage(thread_, this, image, format, callback_id);
|
||||
return callback_id;
|
||||
}
|
||||
|
||||
std::unique_ptr<Skwasm::TextureSourceWrapper>
|
||||
Skwasm::Surface::CreateTextureSourceWrapper(
|
||||
Skwasm::SkwasmObject texture_source) {
|
||||
return std::unique_ptr<Skwasm::TextureSourceWrapper>(
|
||||
new Skwasm::TextureSourceWrapper(thread_, texture_source));
|
||||
}
|
||||
|
||||
// Main thread only
|
||||
void Skwasm::Surface::SetCallbackHandler(
|
||||
Skwasm::Surface::CallbackHandler* callback_handler) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_ = callback_handler;
|
||||
}
|
||||
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::ReceiveCanvasOnWorker(SkwasmObject canvas,
|
||||
uint32_t callback_id) {
|
||||
if (render_context_) {
|
||||
render_context_.reset();
|
||||
}
|
||||
canvas_width_ = 0;
|
||||
canvas_height_ = 0;
|
||||
gl_context_ = skwasm_getGlContextForCanvas(canvas, this);
|
||||
void Skwasm::Surface::Init() {
|
||||
// 256x256 is just an arbitrary size for the initial canvas, so that we can
|
||||
// get a gl context off of it.
|
||||
gl_context_ = skwasm_createOffscreenCanvas(256, 256);
|
||||
if (!gl_context_) {
|
||||
printf("Failed to create context!\n");
|
||||
return;
|
||||
@ -118,35 +112,12 @@ void Skwasm::Surface::ReceiveCanvasOnWorker(SkwasmObject canvas,
|
||||
emscripten_glGetIntegerv(GL_STENCIL_BITS, &stencil);
|
||||
|
||||
render_context_ = Skwasm::RenderContext::Make(sample_count, stencil);
|
||||
render_context_->Resize(canvas_width_, canvas_height_);
|
||||
render_context_->Resize(256, 256);
|
||||
|
||||
context_lost_callback_id_ = ++current_callback_id_;
|
||||
|
||||
skwasm_reportInitialized(this, context_lost_callback_id_, callback_id);
|
||||
}
|
||||
|
||||
// Resizing
|
||||
|
||||
uint32_t Skwasm::Surface::SetSize(int width, int height) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
|
||||
skwasm_dispatchResizeSurface(thread_, this, width, height, callback_id);
|
||||
return callback_id;
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnResizeComplete(uint32_t callback_id) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, nullptr, __builtin_wasm_ref_null_extern());
|
||||
}
|
||||
|
||||
void Skwasm::Surface::ResizeOnWorker(int width,
|
||||
int height,
|
||||
uint32_t callback_id) {
|
||||
ResizeSurface(width, height);
|
||||
skwasm_reportResizeComplete(this, callback_id);
|
||||
is_initialized_ = true;
|
||||
}
|
||||
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::ResizeSurface(int width, int height) {
|
||||
if (width != canvas_width_ || height != canvas_height_) {
|
||||
canvas_width_ = width;
|
||||
@ -155,43 +126,35 @@ void Skwasm::Surface::ResizeSurface(int width, int height) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
uint32_t Skwasm::Surface::RenderPictures(flutter::DisplayList** pictures,
|
||||
int count) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
std::unique_ptr<sk_sp<flutter::DisplayList>[]> picture_pointers =
|
||||
std::make_unique<sk_sp<flutter::DisplayList>[]>(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
picture_pointers[i] = sk_ref_sp(pictures[i]);
|
||||
}
|
||||
|
||||
// Releasing picture_pointers here and will recreate the unique_ptr on the
|
||||
// other thread See surface_renderPicturesOnWorker
|
||||
skwasm_dispatchRenderPictures(thread_, this, picture_pointers.release(),
|
||||
count, callback_id);
|
||||
return callback_id;
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnRenderComplete(uint32_t callback_id,
|
||||
SkwasmObject image_bitmap) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, nullptr, image_bitmap);
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::RecreateSurface() {
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
skwasm_resizeCanvas(gl_context_, canvas_width_, canvas_height_);
|
||||
render_context_->Resize(canvas_width_, canvas_height_);
|
||||
}
|
||||
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::RenderPicturesOnWorker(
|
||||
sk_sp<flutter::DisplayList>* pictures,
|
||||
int width,
|
||||
int height,
|
||||
int picture_count,
|
||||
uint32_t callback_id,
|
||||
double raster_start) {
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
if (!is_initialized_) {
|
||||
Init();
|
||||
}
|
||||
|
||||
// This is initialized on the first call to `skwasm_captureImageBitmap` and
|
||||
// then populated with more bitmaps on subsequent calls.
|
||||
SkwasmObject image_bitmap_array = __builtin_wasm_ref_null_extern();
|
||||
Skwasm::SkwasmObject image_bitmap_array = __builtin_wasm_ref_null_extern();
|
||||
for (int i = 0; i < picture_count; i++) {
|
||||
sk_sp<flutter::DisplayList> picture = pictures[i];
|
||||
ResizeSurface(width, height);
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
|
||||
render_context_->RenderPicture(picture);
|
||||
|
||||
image_bitmap_array =
|
||||
skwasm_captureImageBitmap(gl_context_, image_bitmap_array);
|
||||
}
|
||||
@ -199,30 +162,17 @@ void Skwasm::Surface::RenderPicturesOnWorker(
|
||||
callback_id);
|
||||
}
|
||||
|
||||
// Image Rasterization
|
||||
|
||||
uint32_t Skwasm::Surface::RasterizeImage(flutter::DlImage* image,
|
||||
Skwasm::ImageByteFormat format) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
image->ref();
|
||||
|
||||
skwasm_dispatchRasterizeImage(thread_, this, image, format, callback_id);
|
||||
return callback_id;
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnRasterizeComplete(uint32_t callback_id, SkData* data) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, data, __builtin_wasm_ref_null_extern());
|
||||
}
|
||||
|
||||
// Worker thread only
|
||||
void Skwasm::Surface::RasterizeImageOnWorker(flutter::DlImage* image,
|
||||
Skwasm::ImageByteFormat format,
|
||||
uint32_t callback_id) {
|
||||
if (!is_initialized_) {
|
||||
Init();
|
||||
}
|
||||
|
||||
// We handle PNG encoding with browser APIs so that we can omit libpng from
|
||||
// skia to save binary size.
|
||||
assert(format != Skwasm::ImageByteFormat::png);
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
SkAlphaType alpha_type = format == Skwasm::ImageByteFormat::rawStraightRgba
|
||||
? SkAlphaType::kUnpremul_SkAlphaType
|
||||
: SkAlphaType::kPremul_SkAlphaType;
|
||||
@ -242,6 +192,8 @@ void Skwasm::Surface::RasterizeImageOnWorker(flutter::DlImage* image,
|
||||
// `glReadPixels`. Once the skia bug is fixed, we should switch back to using
|
||||
// `SkImage::readPixels` instead.
|
||||
// See https://g-issues.skia.org/issues/349201915
|
||||
ResizeSurface(image->width(), image->height());
|
||||
|
||||
render_context_->RenderImage(image, format);
|
||||
|
||||
emscripten_glReadPixels(0, 0, image->width(), image->height(), GL_RGBA,
|
||||
@ -251,145 +203,41 @@ void Skwasm::Surface::RasterizeImageOnWorker(flutter::DlImage* image,
|
||||
skwasm_postRasterizeResult(this, data.release(), callback_id);
|
||||
}
|
||||
|
||||
// Context Loss
|
||||
void Skwasm::Surface::OnRasterizeComplete(uint32_t callback_id, SkData* data) {
|
||||
callback_handler_(callback_id, data, __builtin_wasm_ref_null_extern());
|
||||
}
|
||||
|
||||
uint32_t Skwasm::Surface::TriggerContextLoss() {
|
||||
// Main thread only
|
||||
void Skwasm::Surface::OnRenderComplete(uint32_t callback_id,
|
||||
Skwasm::SkwasmObject image_bitmap) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
uint32_t callback_id = ++current_callback_id_;
|
||||
skwasm_dispatchTriggerContextLoss(thread_, this, callback_id);
|
||||
return callback_id;
|
||||
callback_handler_(callback_id, nullptr, image_bitmap);
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnContextLossTriggered(uint32_t callback_id) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, nullptr, __builtin_wasm_ref_null_extern());
|
||||
}
|
||||
|
||||
void Skwasm::Surface::ReportContextLost(uint32_t callback_id) {
|
||||
assert(emscripten_is_main_browser_thread());
|
||||
callback_handler_(callback_id, nullptr, __builtin_wasm_ref_null_extern());
|
||||
}
|
||||
|
||||
void Skwasm::Surface::TriggerContextLossOnWorker(uint32_t callback_id) {
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
skwasm_triggerContextLossOnCanvas();
|
||||
skwasm_reportContextLossTriggered(this, callback_id);
|
||||
}
|
||||
|
||||
void Skwasm::Surface::OnContextLost() {
|
||||
if (!context_lost_callback_id_) {
|
||||
printf("Received context lost event but never set callback handler!\n");
|
||||
return;
|
||||
}
|
||||
skwasm_reportContextLost(this, context_lost_callback_id_);
|
||||
}
|
||||
|
||||
// Other
|
||||
|
||||
void Skwasm::Surface::SetResourceCacheLimit(int bytes) {
|
||||
render_context_->SetResourceCacheLimit(bytes);
|
||||
}
|
||||
|
||||
std::unique_ptr<Skwasm::TextureSourceWrapper>
|
||||
Skwasm::Surface::CreateTextureSourceWrapper(SkwasmObject texture_source) {
|
||||
return std::unique_ptr<Skwasm::TextureSourceWrapper>(
|
||||
new Skwasm::TextureSourceWrapper(thread_, texture_source));
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
void Skwasm::Surface::RecreateSurface() {
|
||||
Skwasm::makeCurrent(gl_context_);
|
||||
skwasm_resizeCanvas(gl_context_, canvas_width_, canvas_height_);
|
||||
render_context_->Resize(canvas_width_, canvas_height_);
|
||||
}
|
||||
|
||||
// TextureSourceWrapper implementation
|
||||
|
||||
Skwasm::TextureSourceWrapper::TextureSourceWrapper(unsigned long thread_id,
|
||||
SkwasmObject texture_source)
|
||||
Skwasm::TextureSourceWrapper::TextureSourceWrapper(
|
||||
unsigned long thread_id,
|
||||
Skwasm::SkwasmObject texture_source)
|
||||
: raster_thread_id_(thread_id) {
|
||||
skwasm_setAssociatedObjectOnThread(thread_id, this, texture_source);
|
||||
skwasm_setAssociatedObjectOnThread(raster_thread_id_, this, texture_source);
|
||||
}
|
||||
|
||||
Skwasm::TextureSourceWrapper::~TextureSourceWrapper() {
|
||||
skwasm_disposeAssociatedObjectOnThread(raster_thread_id_, this);
|
||||
}
|
||||
|
||||
SkwasmObject Skwasm::TextureSourceWrapper::GetTextureSource() {
|
||||
Skwasm::SkwasmObject Skwasm::TextureSourceWrapper::GetTextureSource() {
|
||||
return skwasm_getAssociatedObject(this);
|
||||
}
|
||||
|
||||
// C-style API
|
||||
|
||||
SKWASM_EXPORT Skwasm::Surface* surface_create() {
|
||||
Skwasm::live_surface_count++;
|
||||
return new Skwasm::Surface();
|
||||
}
|
||||
|
||||
SKWASM_EXPORT uint32_t surface_setCanvas(Skwasm::Surface* surface,
|
||||
SkwasmObject canvas) {
|
||||
// Dispatch to the worker so the canvas can be transferred to the worker.
|
||||
return surface->SetCanvas(canvas);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_receiveCanvasOnWorker(Skwasm::Surface* surface,
|
||||
SkwasmObject canvas,
|
||||
uint32_t callback_id) {
|
||||
surface->ReceiveCanvasOnWorker(canvas, callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_onInitialized(Skwasm::Surface* surface,
|
||||
uint32_t callback_id) {
|
||||
surface->OnInitialized(callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT uint32_t surface_setSize(Skwasm::Surface* surface,
|
||||
int width,
|
||||
int height) {
|
||||
return surface->SetSize(width, height);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_resizeOnWorker(Skwasm::Surface* surface,
|
||||
int width,
|
||||
int height,
|
||||
uint32_t callback_id) {
|
||||
surface->ResizeOnWorker(width, height, callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_onResizeComplete(Skwasm::Surface* surface,
|
||||
uint32_t callback_id) {
|
||||
surface->OnResizeComplete(callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT unsigned long surface_getThreadId(Skwasm::Surface* surface) {
|
||||
return surface->GetThreadId();
|
||||
}
|
||||
|
||||
SKWASM_EXPORT EMSCRIPTEN_WEBGL_CONTEXT_HANDLE
|
||||
surface_getGlContext(Skwasm::Surface* surface) {
|
||||
return surface->GetGlContext();
|
||||
}
|
||||
|
||||
SKWASM_EXPORT uint32_t surface_triggerContextLoss(Skwasm::Surface* surface) {
|
||||
return surface->TriggerContextLoss();
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_triggerContextLossOnWorker(Skwasm::Surface* surface,
|
||||
uint32_t callback_id) {
|
||||
surface->TriggerContextLossOnWorker(callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_onContextLossTriggered(Skwasm::Surface* surface,
|
||||
uint32_t callback_id) {
|
||||
surface->OnContextLossTriggered(callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_reportContextLost(Skwasm::Surface* surface,
|
||||
uint32_t callback_id) {
|
||||
surface->ReportContextLost(callback_id);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_setCallbackHandler(
|
||||
Skwasm::Surface* surface,
|
||||
Skwasm::Surface::CallbackHandler* callback_handler) {
|
||||
@ -414,21 +262,25 @@ SKWASM_EXPORT void surface_setResourceCacheLimitBytes(Skwasm::Surface* surface,
|
||||
|
||||
SKWASM_EXPORT uint32_t surface_renderPictures(Skwasm::Surface* surface,
|
||||
flutter::DisplayList** pictures,
|
||||
int width,
|
||||
int height,
|
||||
int count) {
|
||||
return surface->RenderPictures(pictures, count);
|
||||
return surface->RenderPictures(pictures, width, height, count);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_renderPicturesOnWorker(
|
||||
Skwasm::Surface* surface,
|
||||
sk_sp<flutter::DisplayList>* pictures,
|
||||
int width,
|
||||
int height,
|
||||
int picture_count,
|
||||
uint32_t callback_id,
|
||||
double raster_start) {
|
||||
// This will release the pictures when they leave scope.
|
||||
std::unique_ptr<sk_sp<flutter::DisplayList>[]> pictures_pointer =
|
||||
std::unique_ptr<sk_sp<flutter::DisplayList>[]>(pictures);
|
||||
surface->RenderPicturesOnWorker(pictures, picture_count, callback_id,
|
||||
raster_start);
|
||||
surface->RenderPicturesOnWorker(pictures, width, height, picture_count,
|
||||
callback_id, raster_start);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT uint32_t surface_rasterizeImage(Skwasm::Surface* surface,
|
||||
@ -449,7 +301,7 @@ SKWASM_EXPORT void surface_rasterizeImageOnWorker(
|
||||
// we finish creating the image bitmap, which is an asynchronous operation.
|
||||
SKWASM_EXPORT void surface_onRenderComplete(Skwasm::Surface* surface,
|
||||
uint32_t callback_id,
|
||||
SkwasmObject image_bitmap) {
|
||||
Skwasm::SkwasmObject image_bitmap) {
|
||||
surface->OnRenderComplete(callback_id, image_bitmap);
|
||||
}
|
||||
|
||||
@ -459,10 +311,6 @@ SKWASM_EXPORT void surface_onRasterizeComplete(Skwasm::Surface* surface,
|
||||
surface->OnRasterizeComplete(callback_id, data);
|
||||
}
|
||||
|
||||
SKWASM_EXPORT void surface_onContextLost(Skwasm::Surface* surface) {
|
||||
surface->OnContextLost();
|
||||
}
|
||||
|
||||
SKWASM_EXPORT bool skwasm_isMultiThreaded() {
|
||||
return !skwasm_isSingleThreaded();
|
||||
}
|
||||
|
||||
@ -5,16 +5,21 @@
|
||||
#ifndef FLUTTER_SKWASM_SURFACE_H_
|
||||
#define FLUTTER_SKWASM_SURFACE_H_
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/html5_webgl.h>
|
||||
#include <emscripten/threading.h>
|
||||
#include <webgl/webgl1.h>
|
||||
#include <cassert>
|
||||
#include "export.h"
|
||||
#include "render_context.h"
|
||||
#include "wrappers.h"
|
||||
|
||||
#include "flutter/display_list/image/dl_image.h"
|
||||
#include "flutter/skwasm/export.h"
|
||||
#include "flutter/skwasm/render_context.h"
|
||||
#include "flutter/skwasm/wrappers.h"
|
||||
#include "third_party/skia/include/core/SkData.h"
|
||||
#include "third_party/skia/include/core/SkRefCnt.h"
|
||||
|
||||
namespace flutter {
|
||||
class DisplayList;
|
||||
@ -40,51 +45,35 @@ class Surface {
|
||||
// Main thread only
|
||||
Surface();
|
||||
|
||||
// General getters
|
||||
unsigned long GetThreadId() { return thread_; }
|
||||
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE GetGlContext() { return gl_context_; }
|
||||
|
||||
// Lifecycle
|
||||
void SetCallbackHandler(CallbackHandler* callback_handler);
|
||||
// Main thread only
|
||||
void Dispose();
|
||||
|
||||
// Surface setup
|
||||
uint32_t SetCanvas(SkwasmObject canvas);
|
||||
void OnInitialized(uint32_t callback_id);
|
||||
void ReceiveCanvasOnWorker(SkwasmObject canvas, uint32_t callback_id);
|
||||
|
||||
// Resizing
|
||||
uint32_t SetSize(int width, int height);
|
||||
void OnResizeComplete(uint32_t callback_id);
|
||||
void ResizeOnWorker(int width, int height, uint32_t callback_id);
|
||||
|
||||
// Rendering
|
||||
uint32_t RenderPictures(flutter::DisplayList** picture, int count);
|
||||
void SetResourceCacheLimit(int bytes);
|
||||
uint32_t RenderPictures(flutter::DisplayList** pictures,
|
||||
int width,
|
||||
int height,
|
||||
int count);
|
||||
uint32_t RasterizeImage(flutter::DlImage* image, ImageByteFormat format);
|
||||
void SetCallbackHandler(CallbackHandler* callback_handler);
|
||||
void OnRenderComplete(uint32_t callback_id, SkwasmObject image_bitmap);
|
||||
void RenderPicturesOnWorker(sk_sp<flutter::DisplayList>* picture,
|
||||
void OnRasterizeComplete(uint32_t callback_id, SkData* data);
|
||||
|
||||
// Any thread
|
||||
std::unique_ptr<TextureSourceWrapper> CreateTextureSourceWrapper(
|
||||
SkwasmObject texture_source);
|
||||
|
||||
// Worker thread
|
||||
void RenderPicturesOnWorker(sk_sp<flutter::DisplayList>* pictures,
|
||||
int width,
|
||||
int height,
|
||||
int picture_count,
|
||||
uint32_t callback_id,
|
||||
double raster_start);
|
||||
|
||||
// Image Rasterization
|
||||
uint32_t RasterizeImage(flutter::DlImage* image, ImageByteFormat format);
|
||||
void OnRasterizeComplete(uint32_t callback_id, SkData* data);
|
||||
void RasterizeImageOnWorker(flutter::DlImage* image,
|
||||
ImageByteFormat format,
|
||||
uint32_t callback_id);
|
||||
|
||||
// Context Loss
|
||||
uint32_t TriggerContextLoss();
|
||||
void OnContextLossTriggered(uint32_t callback_id);
|
||||
void ReportContextLost(uint32_t callback_id);
|
||||
void TriggerContextLossOnWorker(uint32_t callback_id);
|
||||
void OnContextLost();
|
||||
|
||||
// Other
|
||||
void SetResourceCacheLimit(int bytes);
|
||||
std::unique_ptr<TextureSourceWrapper> CreateTextureSourceWrapper(
|
||||
SkwasmObject textureSource);
|
||||
|
||||
private:
|
||||
void Init();
|
||||
void ResizeSurface(int width, int height);
|
||||
@ -98,8 +87,6 @@ class Surface {
|
||||
|
||||
EMSCRIPTEN_WEBGL_CONTEXT_HANDLE gl_context_ = 0;
|
||||
std::unique_ptr<RenderContext> render_context_;
|
||||
uint32_t context_lost_callback_id_ = 0;
|
||||
|
||||
unsigned long thread_;
|
||||
|
||||
bool is_initialized_ = false;
|
||||
|
||||
@ -29,7 +29,7 @@ inline void makeCurrent(EMSCRIPTEN_WEBGL_CONTEXT_HANDLE handle) {
|
||||
|
||||
int result = emscripten_webgl_make_context_current(handle);
|
||||
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
|
||||
printf("make_context_current failed: %d", result);
|
||||
printf("make_context failed: %d", result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user