diff --git a/DEPS b/DEPS index 0bbbfc8c184f4..db1757ae18bb3 100644 --- a/DEPS +++ b/DEPS @@ -783,7 +783,7 @@ deps = { }, 'src/buildtools/emsdk': { - 'url': Var('skia_git') + '/external/github.com/emscripten-core/emsdk.git' + '@' + 'da9699832b5df4e123403490e499c87000c22654', + 'url': Var('skia_git') + '/external/github.com/emscripten-core/emsdk.git' + '@' + 'a896e3d066448b3530dbcaa48869fafefd738f57', 'condition': 'download_emsdk', }, diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 6af3864bfeb6e..ae517fb56966d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2016,6 +2016,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart + ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE @@ -2035,7 +2036,9 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart + ../../../ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_painting.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart + ../../../flutter/LICENSE @@ -2064,7 +2067,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dar ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/layers.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/memory.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart + ../../../flutter/LICENSE @@ -2094,7 +2096,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/r ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/vertices.dart + ../../../flutter/LICENSE @@ -4743,6 +4744,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_promise.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart @@ -4762,7 +4764,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_painting.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/scene_view.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart @@ -4791,7 +4795,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/filters.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/font_collection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/image.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/layers.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/memory.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paint.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart @@ -4821,7 +4824,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_strut_style.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/text/raw_text_style.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/shaders.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/vertices.dart diff --git a/lib/web_ui/dev/steps/run_suite_step.dart b/lib/web_ui/dev/steps/run_suite_step.dart index b78e6ac9d71be..adb9a12d8bb4b 100644 --- a/lib/web_ui/dev/steps/run_suite_step.dart +++ b/lib/web_ui/dev/steps/run_suite_step.dart @@ -179,8 +179,12 @@ class RunSuiteStep implements PipelineStep { Future _createSkiaClient() async { final Renderer renderer = suite.testBundle.compileConfig.renderer; final CanvasKitVariant? variant = suite.runConfig.variant; + final io.Directory workDirectory = getSkiaGoldDirectoryForSuite(suite); + if (workDirectory.existsSync()) { + workDirectory.deleteSync(recursive: true); + } final SkiaGoldClient skiaClient = SkiaGoldClient( - getSkiaGoldDirectoryForSuite(suite), + workDirectory, dimensions: { 'Browser': suite.runConfig.browser.name, if (isWasm) 'Wasm': 'true', diff --git a/lib/web_ui/dev/test_dart2wasm.js b/lib/web_ui/dev/test_dart2wasm.js index ea30e6e16758c..b48e2403a3535 100644 --- a/lib/web_ui/dev/test_dart2wasm.js +++ b/lib/web_ui/dev/test_dart2wasm.js @@ -64,7 +64,7 @@ window.onload = async function () { const skwasmInstance = await skwasm(); window._flutter_skwasmInstance = skwasmInstance; resolve({ - "skwasm": skwasmInstance.asm, + "skwasm": skwasmInstance.asm ?? skwasmInstance.wasmExports, "ffi": { "memory": skwasmInstance.wasmMemory, } diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 70f3b31de825e..73ecc287611fc 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -110,6 +110,7 @@ export 'engine/js_interop/js_promise.dart'; export 'engine/js_interop/js_typed_data.dart'; export 'engine/key_map.g.dart'; export 'engine/keyboard_binding.dart'; +export 'engine/layers.dart'; export 'engine/mouse_cursor.dart'; export 'engine/navigation/history.dart'; export 'engine/noto_font.dart'; @@ -129,7 +130,9 @@ export 'engine/raw_keyboard.dart'; export 'engine/renderer.dart'; export 'engine/rrect_renderer.dart'; export 'engine/safe_browser_api.dart'; +export 'engine/scene_builder.dart'; export 'engine/scene_painting.dart'; +export 'engine/scene_view.dart'; export 'engine/semantics/accessibility.dart'; export 'engine/semantics/checkable.dart'; export 'engine/semantics/dialog.dart'; diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 4d254e6f0076c..7f8394bc8ffe2 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -530,6 +530,8 @@ extension DomElementExtension on DomElement { external DomElement? get firstElementChild; + external DomElement? get nextElementSibling; + @JS('clientHeight') external JSNumber get _clientHeight; double get clientHeight => _clientHeight.toDartDouble; @@ -1105,6 +1107,9 @@ extension DomCanvasElementExtension on DomCanvasElement { } return getContext('webgl2')! as WebGLContext; } + + DomCanvasRenderingContextBitmapRenderer get contextBitmapRenderer => + getContext('bitmaprenderer')! as DomCanvasRenderingContextBitmapRenderer; } @JS() @@ -1394,6 +1399,15 @@ extension DomCanvasRenderingContextWebGlExtension bool isContextLost() => _isContextLost().toDart; } +@JS() +@staticInterop +class DomCanvasRenderingContextBitmapRenderer {} + +extension DomCanvasRenderingContextBitmapRendererExtension + on DomCanvasRenderingContextBitmapRenderer { + external void transferFromImageBitmap(DomImageBitmap bitmap); +} + @JS('ImageData') @staticInterop class DomImageData { @@ -1409,6 +1423,15 @@ extension DomImageDataExtension on DomImageData { Uint8ClampedList get data => _data.toDart; } +@JS('ImageBitmap') +@staticInterop +class DomImageBitmap {} + +extension DomImageBitmapExtension on DomImageBitmap { + external JSNumber get width; + external JSNumber get height; +} + @JS() @staticInterop class DomCanvasPattern {} diff --git a/lib/web_ui/lib/src/engine/layers.dart b/lib/web_ui/lib/src/engine/layers.dart new file mode 100644 index 0000000000000..e5b52f762779a --- /dev/null +++ b/lib/web_ui/lib/src/engine/layers.dart @@ -0,0 +1,750 @@ +// 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:typed_data'; + +import 'package:meta/meta.dart'; +import 'package:ui/src/engine/scene_painting.dart'; +import 'package:ui/src/engine/vector_math.dart'; +import 'package:ui/ui.dart' as ui; + +class EngineRootLayer with PictureEngineLayer {} + +class BackdropFilterLayer + with PictureEngineLayer + implements ui.BackdropFilterEngineLayer {} +class BackdropFilterOperation implements LayerOperation { + BackdropFilterOperation(this.filter, this.mode); + + final ui.ImageFilter filter; + final ui.BlendMode mode; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect; + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.saveLayerWithFilter(contentRect, ui.Paint()..blendMode = mode, filter); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); +} + +class ClipPathLayer + with PictureEngineLayer + implements ui.ClipPathEngineLayer {} +class ClipPathOperation implements LayerOperation { + ClipPathOperation(this.path, this.clip); + + final ui.Path path; + final ui.Clip clip; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.save(); + canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.saveLayer(path.getBounds(), ui.Paint()); + } + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.restore(); + } + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() { + // TODO(jacksongardner): implement clip styling for platform views + return const PlatformViewStyling(); + } +} + +class ClipRectLayer + with PictureEngineLayer + implements ui.ClipRectEngineLayer {} +class ClipRectOperation implements LayerOperation { + const ClipRectOperation(this.rect, this.clip); + + final ui.Rect rect; + final ui.Clip clip; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(rect); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.save(); + canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.saveLayer(rect, ui.Paint()); + } + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.restore(); + } + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() { + // TODO(jacksongardner): implement clip styling for platform views + return const PlatformViewStyling(); + } +} + +class ClipRRectLayer + with PictureEngineLayer + implements ui.ClipRRectEngineLayer {} +class ClipRRectOperation implements LayerOperation { + const ClipRRectOperation(this.rrect, this.clip); + + final ui.RRect rrect; + final ui.Clip clip; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.save(); + canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.saveLayer(rrect.outerRect, ui.Paint()); + } + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + if (clip == ui.Clip.antiAliasWithSaveLayer) { + canvas.restore(); + } + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() { + // TODO(jacksongardner): implement clip styling for platform views + return const PlatformViewStyling(); + } +} + +class ColorFilterLayer + with PictureEngineLayer + implements ui.ColorFilterEngineLayer {} +class ColorFilterOperation implements LayerOperation { + ColorFilterOperation(this.filter); + + final ui.ColorFilter filter; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect; + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.saveLayer(contentRect, ui.Paint()..colorFilter = filter); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); +} + +class ImageFilterLayer + with PictureEngineLayer + implements ui.ImageFilterEngineLayer {} +class ImageFilterOperation implements LayerOperation { + ImageFilterOperation(this.filter, this.offset); + + final ui.ImageFilter filter; + final ui.Offset offset; + + @override + ui.Rect cullRect(ui.Rect contentRect) => (filter as SceneImageFilter).filterBounds(contentRect); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + if (offset != ui.Offset.zero) { + canvas.save(); + canvas.translate(offset.dx, offset.dy); + } + final ui.Rect adjustedContentRect = + (filter as SceneImageFilter).filterBounds(contentRect); + canvas.saveLayer(adjustedContentRect, ui.Paint()..imageFilter = filter); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + if (offset != ui.Offset.zero) { + canvas.restore(); + } + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() { + if (offset != ui.Offset.zero) { + return PlatformViewStyling( + position: PlatformViewPosition.offset(offset) + ); + } else { + return const PlatformViewStyling(); + } + } +} + +class OffsetLayer + with PictureEngineLayer + implements ui.OffsetEngineLayer {} +class OffsetOperation implements LayerOperation { + OffsetOperation(this.dx, this.dy); + + final double dx; + final double dy; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect.shift(ui.Offset(-dx, -dy)); + + @override + void pre(SceneCanvas canvas, ui.Rect cullRect) { + canvas.save(); + canvas.translate(dx, dy); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() => PlatformViewStyling( + position: PlatformViewPosition.offset(ui.Offset(dx, dy)) + ); +} + +class OpacityLayer + with PictureEngineLayer + implements ui.OpacityEngineLayer {} +class OpacityOperation implements LayerOperation { + OpacityOperation(this.alpha, this.offset); + + final int alpha; + final ui.Offset offset; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect.shift(offset); + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect cullRect) { + if (offset != ui.Offset.zero) { + canvas.save(); + canvas.translate(offset.dx, offset.dy); + } + canvas.saveLayer( + cullRect, + ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) + ); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.restore(); + if (offset != ui.Offset.zero) { + canvas.restore(); + } + } + + @override + PlatformViewStyling createPlatformViewStyling() => PlatformViewStyling( + position: offset != ui.Offset.zero ? PlatformViewPosition.offset(offset) : const PlatformViewPosition.zero(), + opacity: alpha.toDouble() / 255.0, + ); +} + +class TransformLayer + with PictureEngineLayer + implements ui.TransformEngineLayer {} +class TransformOperation implements LayerOperation { + TransformOperation(this.transform); + + final Float64List transform; + + Matrix4 getMatrix() => Matrix4.fromFloat32List(toMatrix32(transform)); + + @override + ui.Rect cullRect(ui.Rect contentRect) => getMatrix().transformRect(contentRect); + + @override + ui.Rect inverseMapRect(ui.Rect rect) { + final Matrix4 matrix = getMatrix()..invert(); + return matrix.transformRect(rect); + } + + @override + void pre(SceneCanvas canvas, ui.Rect cullRect) { + canvas.save(); + canvas.transform(transform); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() => PlatformViewStyling( + position: PlatformViewPosition.transform(getMatrix()), + ); +} + +class ShaderMaskLayer + with PictureEngineLayer + implements ui.ShaderMaskEngineLayer {} +class ShaderMaskOperation implements LayerOperation { + ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); + + final ui.Shader shader; + final ui.Rect maskRect; + final ui.BlendMode blendMode; + + @override + ui.Rect cullRect(ui.Rect contentRect) => contentRect; + + @override + ui.Rect inverseMapRect(ui.Rect rect) => rect; + + @override + void pre(SceneCanvas canvas, ui.Rect contentRect) { + canvas.saveLayer( + contentRect, + ui.Paint(), + ); + } + + @override + void post(SceneCanvas canvas, ui.Rect contentRect) { + canvas.save(); + canvas.translate(maskRect.left, maskRect.top); + canvas.drawRect( + ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), + ui.Paint() + ..blendMode = blendMode + ..shader = shader + ); + canvas.restore(); + canvas.restore(); + } + + @override + PlatformViewStyling createPlatformViewStyling() => const PlatformViewStyling(); +} + +class PlatformView { + PlatformView(this.viewId, this.size, this.styling); + + int viewId; + + // The bounds of this platform view, in the layer's local coordinate space. + ui.Size size; + + PlatformViewStyling styling; +} + +sealed class LayerSlice { + void dispose(); +} + +// A slice that contains one or more platform views to be rendered. +class PlatformViewSlice implements LayerSlice { + PlatformViewSlice(this.views, this.occlusionRect); + + List views; + + // A conservative estimate of what area platform views in this slice may cover. + // This is expressed in the coordinate space of the parent. + ui.Rect? occlusionRect; + + @override + void dispose() {} +} + +// A slice that contains flutter content to be rendered int he form of a single +// ScenePicture. +class PictureSlice implements LayerSlice { + PictureSlice(this.picture); + + ScenePicture picture; + + @override + void dispose() => picture.dispose(); +} + +mixin PictureEngineLayer implements ui.EngineLayer { + // Each layer is represented as a series of "slices" which contain either + // flutter content or platform views. Slices in this list are ordered from + // bottom to top. + List slices = []; + + @override + void dispose() { + for (final LayerSlice slice in slices) { + slice.dispose(); + } + } +} + +abstract class LayerOperation { + const LayerOperation(); + + // Given an input content rectangle, this returns a conservative estimate of + // the covering rectangle of the content after it has been processed by the + // layer operation. + ui.Rect cullRect(ui.Rect contentRect); + + // Takes a rectangle in the layer's coordinate space and maps it to the parent + // coordinate space. + ui.Rect inverseMapRect(ui.Rect rect); + void pre(SceneCanvas canvas, ui.Rect contentRect); + void post(SceneCanvas canvas, ui.Rect contentRect); + + PlatformViewStyling createPlatformViewStyling(); +} + +class PictureDrawCommand { + PictureDrawCommand(this.offset, this.picture); + + ui.Offset offset; + ui.Picture picture; +} + +// Represents how a platform view should be positioned in the scene. +// This object is immutable, so it can be reused across different platform +// views that have the same positioning. +class PlatformViewPosition { + // No transformation at all. We leave both fields null. + const PlatformViewPosition.zero() : offset = null, transform = null; + + // A simple offset is the most common scenario. In those cases, we only + // store the offset and leave the transform as null + const PlatformViewPosition.offset(this.offset) : transform = null; + + // In more complex cases, we store the transform. In those cases, the offset + // is left as null. + const PlatformViewPosition.transform(this.transform) : offset = null; + + bool get isZero => (offset == null) && (transform == null); + + final ui.Offset? offset; + final Matrix4? transform; + + static PlatformViewPosition combine(PlatformViewPosition outer, PlatformViewPosition inner) { + // We try to reuse existing objects if possible, if they are immutable. + if (outer.isZero) { + return inner; + } + if (inner.isZero) { + return outer; + } + final ui.Offset? outerOffset = outer.offset; + final ui.Offset? innerOffset = inner.offset; + if (outerOffset != null && innerOffset != null) { + // Both positions are simple offsets, so they can be combined cheaply + // into another offset. + return PlatformViewPosition.offset(outerOffset + innerOffset); + } + + // Otherwise, at least one of the positions involves a matrix transform. + final Matrix4 newTransform; + if (outerOffset != null) { + newTransform = Matrix4.translationValues(outerOffset.dx, outerOffset.dy, 0); + } else { + newTransform = outer.transform!.clone(); + } + if (innerOffset != null) { + newTransform.translate(innerOffset.dx, innerOffset.dy); + } else { + newTransform.multiply(inner.transform!); + } + return PlatformViewPosition.transform(newTransform); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! PlatformViewPosition) { + return false; + } + return (offset == other.offset) && (transform == other.transform); + } + + @override + int get hashCode { + return Object.hash(offset, transform); + } +} + +// Represents the styling to be performed on a platform view when it is +// composited. This object is immutable so that it can be reused with different +// platform views that have the same styling. +class PlatformViewStyling { + const PlatformViewStyling({ + this.position = const PlatformViewPosition.zero(), + this.opacity = 1.0 + }); + + bool get isDefault => position.isZero && (opacity == 1.0); + + final PlatformViewPosition position; + final double opacity; + + static PlatformViewStyling combine(PlatformViewStyling outer, PlatformViewStyling inner) { + // Attempt to reuse one of the existing immutable objects. + if (outer.isDefault) { + return inner; + } + if (inner.isDefault) { + return outer; + } + return PlatformViewStyling( + position: PlatformViewPosition.combine(outer.position, inner.position), + opacity: outer.opacity * inner.opacity, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! PlatformViewStyling) { + return false; + } + return (position == other.position) && (opacity == other.opacity); + } + + @override + int get hashCode { + return Object.hash(position, opacity); + } +} + +class LayerBuilder { + factory LayerBuilder.rootLayer() { + return LayerBuilder._(null, EngineRootLayer(), null); + } + + factory LayerBuilder.childLayer({ + required LayerBuilder parent, + required PictureEngineLayer layer, + required LayerOperation operation + }) { + return LayerBuilder._(parent, layer, operation); + } + + LayerBuilder._( + this.parent, + this.layer, + this.operation); + + @visibleForTesting + static (ui.PictureRecorder, SceneCanvas) Function(ui.Rect)? debugRecorderFactory; + + final LayerBuilder? parent; + final PictureEngineLayer layer; + final LayerOperation? operation; + final List pendingPictures = []; + List pendingPlatformViews = []; + ui.Rect? picturesRect; + ui.Rect? platformViewRect; + + PlatformViewStyling? _memoizedPlatformViewStyling; + + PlatformViewStyling _createPlatformViewStyling() { + final PlatformViewStyling? innerStyling = operation?.createPlatformViewStyling(); + final PlatformViewStyling? outerStyling = parent?.platformViewStyling; + if (innerStyling == null) { + return outerStyling ?? const PlatformViewStyling(); + } + if (outerStyling == null) { + return innerStyling; + } + return PlatformViewStyling.combine(outerStyling, innerStyling); + } + + PlatformViewStyling get platformViewStyling { + return _memoizedPlatformViewStyling ??= _createPlatformViewStyling(); + } + + (ui.PictureRecorder, SceneCanvas) _createRecorder(ui.Rect rect) { + if (debugRecorderFactory != null) { + return debugRecorderFactory!(rect); + } + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; + return (recorder, canvas); + } + + void flushSlices() { + if (pendingPictures.isNotEmpty) { + // Merge the existing draw commands into a single picture and add a slice + // with that picture to the slice list. + final ui.Rect drawnRect = picturesRect ?? ui.Rect.zero; + final ui.Rect rect = operation?.cullRect(drawnRect) ?? drawnRect; + final (ui.PictureRecorder recorder, SceneCanvas canvas) = _createRecorder(rect); + + operation?.pre(canvas, rect); + for (final PictureDrawCommand command in pendingPictures) { + if (command.offset != ui.Offset.zero) { + canvas.save(); + canvas.translate(command.offset.dx, command.offset.dy); + canvas.drawPicture(command.picture); + canvas.restore(); + } else { + canvas.drawPicture(command.picture); + } + } + operation?.post(canvas, rect); + final ui.Picture picture = recorder.endRecording(); + layer.slices.add(PictureSlice(picture as ScenePicture)); + } + + if (pendingPlatformViews.isNotEmpty) { + // Take any pending platform views and lower them into a platform view + // slice. + ui.Rect? occlusionRect = platformViewRect; + if (occlusionRect != null && operation != null) { + occlusionRect = operation!.inverseMapRect(occlusionRect); + } + layer.slices.add(PlatformViewSlice(pendingPlatformViews, occlusionRect)); + } + + pendingPictures.clear(); + pendingPlatformViews = []; + + // All the pictures and platform views have been lowered into slices. Clear + // our occlusion rectangles. + picturesRect = null; + platformViewRect = null; + } + + void addPicture( + ui.Offset offset, + ui.Picture picture, { + bool isComplexHint = false, + bool willChangeHint = false + }) { + final ui.Rect cullRect = (picture as ScenePicture).cullRect; + final ui.Rect shiftedRect = cullRect.shift(offset); + + final ui.Rect? currentPlatformViewRect = platformViewRect; + if (currentPlatformViewRect != null) { + // Whenever we add a picture to our layer, we try to see if the picture + // will overlap with any platform views that are currently on top of our + // drawing surface. If they don't overlap with the platform views, they + // can be grouped with the existing pending pictures. + if (pendingPictures.isEmpty || currentPlatformViewRect.overlaps(shiftedRect)) { + // If they do overlap with the platform views, however, we should flush + // all the current content into slices and start anew with a fresh + // group of pictures and platform views that will be rendered on top of + // the previous content. Note that we also flush if we have no pending + // pictures to group with. This is the case when platform views are + // the first thing in our stack of objects to composite, and it doesn't + // make sense to try to put a picture slice below the first platform + // view slice, even if the picture doesn't overlap. + flushSlices(); + } + } + pendingPictures.add(PictureDrawCommand(offset, picture)); + picturesRect = picturesRect?.expandToInclude(shiftedRect) ?? shiftedRect; + } + + void addPlatformView( + int viewId, { + ui.Offset offset = ui.Offset.zero, + double width = 0.0, + double height = 0.0 + }) { + final ui.Rect bounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + platformViewRect = platformViewRect?.expandToInclude(bounds) ?? bounds; + final PlatformViewStyling layerStyling = platformViewStyling; + final PlatformViewStyling viewStyling = offset == ui.Offset.zero + ? layerStyling + : PlatformViewStyling.combine( + layerStyling, + PlatformViewStyling( + position: PlatformViewPosition.offset(offset), + ), + ); + pendingPlatformViews.add(PlatformView(viewId, ui.Size(width, height), viewStyling)); + } + + void mergeLayer(PictureEngineLayer layer) { + // When we merge layers, we attempt to merge slices as much as possible as + // well, based on ordering of pictures and platform views and reusing the + // occlusion logic for determining where we can lower each picture. + for (final LayerSlice slice in layer.slices) { + switch (slice) { + case PictureSlice(): + addPicture(ui.Offset.zero, slice.picture); + case PlatformViewSlice(): + final ui.Rect? occlusionRect = slice.occlusionRect; + if (occlusionRect != null) { + platformViewRect = platformViewRect?.expandToInclude(occlusionRect) ?? occlusionRect; + } + pendingPlatformViews.addAll(slice.views); + } + } + } + + PictureEngineLayer build() { + // Lower any pending pictures or platform views to their respective slices. + flushSlices(); + return layer; + } +} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart b/lib/web_ui/lib/src/engine/scene_builder.dart similarity index 66% rename from lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart rename to lib/web_ui/lib/src/engine/scene_builder.dart index 4cee3683a3178..2ec9f6bc7523d 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/scene_builder.dart @@ -4,32 +4,73 @@ import 'dart:typed_data'; -import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -class SkwasmScene implements ui.Scene { - SkwasmScene(this.picture); +// This file implements a SceneBuilder and Scene that works with any renderer +// implementation that provides: +// * A `ui.Canvas` that conforms to `SceneCanvas` +// * A `ui.Picture` that conforms to `ScenePicture` +// * A `ui.ImageFilter` that conforms to `SceneImageFilter` +// +// These contain a few augmentations to the normal `dart:ui` API that provide +// additional sizing information that the scene builder uses to determine how +// these object might occlude one another. - final ui.Picture picture; + +class EngineScene implements ui.Scene { + EngineScene(this.rootLayer); + + final EngineRootLayer rootLayer; + + // We keep a refcount here because this can be asynchronously rendered, so we + // don't necessarily want to dispose immediately when the user calls dispose. + // Instead, we need to stay alive until we're done rendering. + int _refCount = 1; + + void beginRender() { + assert(_refCount > 0); + _refCount++; + } + + void endRender() { + _refCount--; + _disposeIfNeeded(); + } @override void dispose() { - picture.dispose(); + _refCount--; + _disposeIfNeeded(); + } + + void _disposeIfNeeded() { + assert(_refCount >= 0); + if (_refCount == 0) { + rootLayer.dispose(); + } } @override - Future toImage(int width, int height) { - return picture.toImage(width, height); + Future toImage(int width, int height) async { + return toImageSync(width, height); } @override ui.Image toImageSync(int width, int height) { - return picture.toImageSync(width, height); - } + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Rect canvasRect = ui.Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()); + final ui.Canvas canvas = ui.Canvas(recorder, canvasRect); + // Only rasterizes the picture slices. + for (final PictureSlice slice in rootLayer.slices.whereType()) { + canvas.drawPicture(slice.picture); + } + return recorder.endRecording().toImageSync(width, height); + } } -class SkwasmSceneBuilder implements ui.SceneBuilder { +class EngineSceneBuilder implements ui.SceneBuilder { LayerBuilder currentBuilder = LayerBuilder.rootLayer(); @override @@ -61,16 +102,17 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { double width = 0.0, double height = 0.0 }) { - throw UnimplementedError('Platform view not yet implemented with skwasm renderer.'); + currentBuilder.addPlatformView( + viewId, + offset: offset, + width: width, + height: height + ); } @override void addRetained(ui.EngineLayer retainedLayer) { - final ui.Picture? picture = (retainedLayer as PictureLayer).picture; - if (picture == null) { - throw StateError('Adding incomplete retained layer.'); - } - currentBuilder.addPicture(ui.Offset.zero, picture); + currentBuilder.mergeLayer(retainedLayer as PictureEngineLayer); } @override @@ -82,7 +124,7 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { bool freeze = false, ui.FilterQuality filterQuality = ui.FilterQuality.low }) { - // TODO(jacksongardner): implement addTexture + // addTexture is not implemented on web. } @override @@ -187,10 +229,12 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { @override void setCheckerboardOffscreenLayers(bool checkerboard) { + // Not implemented on web } @override void setCheckerboardRasterCacheImages(bool checkerboard) { + // Not implemented on web } @override @@ -203,10 +247,12 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { double insetLeft, bool focusable ) { + // Not implemented on web } @override void setRasterizerTracingThreshold(int frameInterval) { + // Not implemented on web } @override @@ -214,22 +260,22 @@ class SkwasmSceneBuilder implements ui.SceneBuilder { while (currentBuilder.parent != null) { pop(); } - final ui.Picture finalPicture = currentBuilder.build(); - return SkwasmScene(finalPicture); + final PictureEngineLayer rootLayer = currentBuilder.build(); + return EngineScene(rootLayer as EngineRootLayer); } @override void pop() { - final ui.Picture picture = currentBuilder.build(); + final PictureEngineLayer layer = currentBuilder.build(); final LayerBuilder? parentBuilder = currentBuilder.parent; if (parentBuilder == null) { throw StateError('Popped too many times.'); } currentBuilder = parentBuilder; - currentBuilder.addPicture(ui.Offset.zero, picture); + currentBuilder.mergeLayer(layer); } - T pushLayer(T layer, LayerOperation operation) { + T pushLayer(T layer, LayerOperation operation) { currentBuilder = LayerBuilder.childLayer( parent: currentBuilder, layer: layer, diff --git a/lib/web_ui/lib/src/engine/scene_view.dart b/lib/web_ui/lib/src/engine/scene_view.dart new file mode 100644 index 0000000000000..a8c4a17307a27 --- /dev/null +++ b/lib/web_ui/lib/src/engine/scene_view.dart @@ -0,0 +1,235 @@ +// 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 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +const String kCanvasContainerTag = 'flt-canvas-container'; + +// This is an interface that renders a `ScenePicture` as a `DomImageBitmap`. +// It is optionally asynchronous. It is required for the `EngineSceneView` to +// composite pictures into the canvases in the DOM tree it builds. +abstract class PictureRenderer { + FutureOr renderPicture(ScenePicture picture); +} + +// This class builds a DOM tree that composites an `EngineScene`. +class EngineSceneView { + factory EngineSceneView(PictureRenderer pictureRenderer) { + final DomElement sceneElement = createDomElement('flt-scene'); + return EngineSceneView._(pictureRenderer, sceneElement); + } + + EngineSceneView._(this.pictureRenderer, this.sceneElement); + + final PictureRenderer pictureRenderer; + final DomElement sceneElement; + + List containers = []; + + int queuedRenders = 0; + static const int kMaxQueuedRenders = 3; + + Future renderScene(EngineScene scene) async { + if (queuedRenders >= kMaxQueuedRenders) { + return; + } + queuedRenders += 1; + + scene.beginRender(); + final List slices = scene.rootLayer.slices; + final Iterable> renderFutures = slices.map( + (LayerSlice slice) async => switch (slice) { + PlatformViewSlice() => null, + PictureSlice() => pictureRenderer.renderPicture(slice.picture), + } + ); + final List renderedBitmaps = await Future.wait(renderFutures); + final List reusableContainers = List.from(containers); + final List newContainers = []; + for (int i = 0; i < slices.length; i++) { + final LayerSlice slice = slices[i]; + switch (slice) { + case PictureSlice(): + PictureSliceContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PictureSliceContainer) { + container = candidate; + reusableContainers[j] = null; + break; + } + } + + if (container != null) { + container.bounds = slice.picture.cullRect; + } else { + container = PictureSliceContainer(slice.picture.cullRect); + } + container.updateContents(); + container.renderBitmap(renderedBitmaps[i]!); + newContainers.add(container); + + case PlatformViewSlice(): + for (final PlatformView view in slice.views) { + // Attempt to reuse a container for the existing view + PlatformViewContainer? container; + for (int j = 0; j < reusableContainers.length; j++) { + final SliceContainer? candidate = reusableContainers[j]; + if (candidate is PlatformViewContainer && candidate.viewId == view.viewId) { + container = candidate; + reusableContainers[j] = null; + break; + } + } + container ??= PlatformViewContainer(view.viewId); + container.size = view.size; + container.styling = view.styling; + container.updateContents(); + newContainers.add(container); + } + } + } + + containers = newContainers; + + DomElement? currentElement = sceneElement.firstElementChild; + for (final SliceContainer container in containers) { + if (currentElement == null) { + sceneElement.appendChild(container.container); + } else if (currentElement == container.container) { + currentElement = currentElement.nextElementSibling; + } else { + sceneElement.insertBefore(container.container, currentElement); + } + } + + // Remove any other unused containers + while (currentElement != null) { + final DomElement? sibling = currentElement.nextElementSibling; + sceneElement.removeChild(currentElement); + currentElement = sibling; + } + scene.endRender(); + + queuedRenders -= 1; + } +} + +sealed class SliceContainer { + DomElement get container; + + void updateContents(); +} + +final class PictureSliceContainer extends SliceContainer { + factory PictureSliceContainer(ui.Rect bounds) { + final DomElement container = domDocument.createElement(kCanvasContainerTag); + final DomCanvasElement canvas = createDomCanvasElement( + width: bounds.width.toInt(), + height: bounds.height.toInt() + ); + container.appendChild(canvas); + return PictureSliceContainer._(bounds, container, canvas); + } + + PictureSliceContainer._(this._bounds, this.container, this.canvas); + + ui.Rect _bounds; + bool _dirty = true; + + ui.Rect get bounds => _bounds; + set bounds(ui.Rect bounds) { + if (_bounds != bounds) { + _bounds = bounds; + _dirty = true; + } + } + + @override + void updateContents() { + if (_dirty) { + _dirty = false; + + final ui.Rect roundedOutBounds = ui.Rect.fromLTRB( + bounds.left.floorToDouble(), + bounds.top.floorToDouble(), + bounds.right.ceilToDouble(), + bounds.bottom.ceilToDouble() + ); + final DomCSSStyleDeclaration style = canvas.style; + final double logicalWidth = roundedOutBounds.width / window.devicePixelRatio; + final double logicalHeight = roundedOutBounds.height / window.devicePixelRatio; + style.width = '${logicalWidth}px'; + style.height = '${logicalHeight}px'; + style.position = 'absolute'; + style.left = '${roundedOutBounds.left}px'; + style.top = '${roundedOutBounds.top}px'; + canvas.width = roundedOutBounds.width.ceilToDouble(); + canvas.height = roundedOutBounds.height.ceilToDouble(); + } + } + + void renderBitmap(DomImageBitmap bitmap) { + final DomCanvasRenderingContextBitmapRenderer ctx = canvas.contextBitmapRenderer; + ctx.transferFromImageBitmap(bitmap); + } + + @override + final DomElement container; + final DomCanvasElement canvas; +} + +final class PlatformViewContainer extends SliceContainer { + PlatformViewContainer(this.viewId) : container = createPlatformViewSlot(viewId); + + final int viewId; + PlatformViewStyling? _styling; + ui.Size? _size; + bool _dirty = false; + + @override + final DomElement container; + + set styling(PlatformViewStyling styling) { + if (_styling != styling) { + _styling = styling; + _dirty = true; + } + } + + set size(ui.Size size) { + if (_size != size) { + _size = size; + _dirty = true; + } + } + + @override + void updateContents() { + assert(_styling != null); + assert(_size != null); + if (_dirty) { + final DomCSSStyleDeclaration style = container.style; + final double logicalWidth = _size!.width / window.devicePixelRatio; + final double logicalHeight = _size!.height / window.devicePixelRatio; + style.width = '${logicalWidth}px'; + style.height = '${logicalHeight}px'; + style.position = 'absolute'; + + final ui.Offset? offset = _styling!.position.offset; + style.left = '${offset?.dx ?? 0}px'; + style.top = '${offset?.dy ?? 0}px'; + + final Matrix4? transform = _styling!.position.transform; + style.transform = transform != null ? float64ListToCssTransform3d(transform.storage) : ''; + style.opacity = _styling!.opacity != 1.0 ? '${_styling!.opacity}' : ''; + // TODO(jacksongardner): Implement clip styling for platform views + + _dirty = false; + } + } +} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart index ab0b58d728387..fd370c7a0fb18 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl.dart @@ -14,7 +14,6 @@ export 'skwasm_impl/codecs.dart'; export 'skwasm_impl/filters.dart'; export 'skwasm_impl/font_collection.dart'; export 'skwasm_impl/image.dart'; -export 'skwasm_impl/layers.dart'; export 'skwasm_impl/memory.dart'; export 'skwasm_impl/paint.dart'; export 'skwasm_impl/paragraph.dart'; @@ -44,7 +43,6 @@ export 'skwasm_impl/raw/text/raw_paragraph_style.dart'; export 'skwasm_impl/raw/text/raw_strut_style.dart'; export 'skwasm_impl/raw/text/raw_text_style.dart'; export 'skwasm_impl/renderer.dart'; -export 'skwasm_impl/scene_builder.dart'; export 'skwasm_impl/shaders.dart'; export 'skwasm_impl/surface.dart'; export 'skwasm_impl/vertices.dart'; diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart index 91eedca9c8000..7c1a91cbd29c7 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/codecs.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:js_interop'; - import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; @@ -20,11 +18,8 @@ class SkwasmImageDecoder extends BrowserImageDecoder { final int width = frame.codedWidth.toInt(); final int height = frame.codedHeight.toInt(); final SkwasmSurface surface = (renderer as SkwasmRenderer).surface; - final int videoFrameId = surface.acquireObjectId(); - skwasmInstance.skwasmRegisterObject(videoFrameId.toJS, frame as JSAny); - skwasmInstance.skwasmTransferObjectToThread(videoFrameId.toJS, surface.threadId.toJS); return SkwasmImage(imageCreateFromVideoFrame( - videoFrameId, + frame, width, height, surface.handle, diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/layers.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/layers.dart deleted file mode 100644 index 12142f04332fa..0000000000000 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/layers.dart +++ /dev/null @@ -1,379 +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:typed_data'; - -import 'package:ui/src/engine/scene_painting.dart'; -import 'package:ui/src/engine/vector_math.dart'; -import 'package:ui/ui.dart' as ui; - -class BackdropFilterLayer - with PictureLayer - implements ui.BackdropFilterEngineLayer {} -class BackdropFilterOperation implements LayerOperation { - BackdropFilterOperation(this.filter, this.mode); - - final ui.ImageFilter filter; - final ui.BlendMode mode; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayerWithFilter(contentRect, ui.Paint()..blendMode = mode, filter); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.restore(); - } -} - -class ClipPathLayer - with PictureLayer - implements ui.ClipPathEngineLayer {} -class ClipPathOperation implements LayerOperation { - ClipPathOperation(this.path, this.clip); - - final ui.Path path; - final ui.Clip clip; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(path.getBounds()); - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.save(); - canvas.clipPath(path, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(path.getBounds(), ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } -} - -class ClipRectLayer - with PictureLayer - implements ui.ClipRectEngineLayer {} -class ClipRectOperation implements LayerOperation { - const ClipRectOperation(this.rect, this.clip); - - final ui.Rect rect; - final ui.Clip clip; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(rect); - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.save(); - canvas.clipRect(rect, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(rect, ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } -} - -class ClipRRectLayer - with PictureLayer - implements ui.ClipRRectEngineLayer {} -class ClipRRectOperation implements LayerOperation { - const ClipRRectOperation(this.rrect, this.clip); - - final ui.RRect rrect; - final ui.Clip clip; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect.intersect(rrect.outerRect); - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.save(); - canvas.clipRRect(rrect, doAntiAlias: clip != ui.Clip.hardEdge); - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.saveLayer(rrect.outerRect, ui.Paint()); - } - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - if (clip == ui.Clip.antiAliasWithSaveLayer) { - canvas.restore(); - } - canvas.restore(); - } -} - -class ColorFilterLayer - with PictureLayer - implements ui.ColorFilterEngineLayer {} -class ColorFilterOperation implements LayerOperation { - ColorFilterOperation(this.filter); - - final ui.ColorFilter filter; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayer(contentRect, ui.Paint()..colorFilter = filter); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.restore(); - } -} - -class ImageFilterLayer - with PictureLayer - implements ui.ImageFilterEngineLayer {} -class ImageFilterOperation implements LayerOperation { - ImageFilterOperation(this.filter, this.offset); - - final ui.ImageFilter filter; - final ui.Offset offset; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - if (offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(offset.dx, offset.dy); - } - final ui.Rect adjustedContentRect = - (filter as SceneImageFilter).filterBounds(contentRect); - canvas.saveLayer(adjustedContentRect, ui.Paint()..imageFilter = filter); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - if (offset != ui.Offset.zero) { - canvas.restore(); - } - canvas.restore(); - } -} - -class OffsetLayer - with PictureLayer - implements ui.OffsetEngineLayer {} -class OffsetOperation implements LayerOperation { - OffsetOperation(this.dx, this.dy); - - final double dx; - final double dy; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect.shift(ui.Offset(dx, dy)); - - @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { - canvas.save(); - canvas.translate(dx, dy); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.restore(); - } -} - -class OpacityLayer - with PictureLayer - implements ui.OpacityEngineLayer {} -class OpacityOperation implements LayerOperation { - OpacityOperation(this.alpha, this.offset); - - final int alpha; - final ui.Offset offset; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect.shift(offset); - - @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { - if (offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(offset.dx, offset.dy); - } - canvas.saveLayer( - cullRect, - ui.Paint()..color = ui.Color.fromARGB(alpha, 0, 0, 0) - ); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.restore(); - if (offset != ui.Offset.zero) { - canvas.restore(); - } - } -} - -class TransformLayer - with PictureLayer - implements ui.TransformEngineLayer {} -class TransformOperation implements LayerOperation { - TransformOperation(this.transform); - - final Float64List transform; - - @override - ui.Rect cullRect(ui.Rect contentRect) => - Matrix4.fromFloat32List(toMatrix32(transform)).transformRect(contentRect); - - @override - void pre(SceneCanvas canvas, ui.Rect cullRect) { - canvas.save(); - canvas.transform(transform); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.restore(); - } -} - -class ShaderMaskLayer - with PictureLayer - implements ui.ShaderMaskEngineLayer {} -class ShaderMaskOperation implements LayerOperation { - ShaderMaskOperation(this.shader, this.maskRect, this.blendMode); - - final ui.Shader shader; - final ui.Rect maskRect; - final ui.BlendMode blendMode; - - @override - ui.Rect cullRect(ui.Rect contentRect) => contentRect; - - @override - void pre(SceneCanvas canvas, ui.Rect contentRect) { - canvas.saveLayer( - contentRect, - ui.Paint(), - ); - } - - @override - void post(SceneCanvas canvas, ui.Rect contentRect) { - canvas.save(); - canvas.translate(maskRect.left, maskRect.top); - canvas.drawRect( - ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), - ui.Paint() - ..blendMode = blendMode - ..shader = shader - ); - canvas.restore(); - canvas.restore(); - } -} - - -mixin PictureLayer implements ui.EngineLayer { - ui.Picture? picture; - - @override - void dispose() { - picture?.dispose(); - } -} - -abstract class LayerOperation { - const LayerOperation(); - - ui.Rect cullRect(ui.Rect contentRect); - void pre(SceneCanvas canvas, ui.Rect contentRect); - void post(SceneCanvas canvas, ui.Rect contentRect); -} - -class PictureDrawCommand { - PictureDrawCommand(this.offset, this.picture); - - ui.Offset offset; - ui.Picture picture; -} - -class LayerBuilder { - factory LayerBuilder.rootLayer() { - return LayerBuilder._(null, null, null); - } - - factory LayerBuilder.childLayer({ - required LayerBuilder parent, - required PictureLayer layer, - required LayerOperation operation - }) { - return LayerBuilder._(parent, layer, operation); - } - - LayerBuilder._( - this.parent, - this.layer, - this.operation - ); - - final LayerBuilder? parent; - final PictureLayer? layer; - final LayerOperation? operation; - final List drawCommands = []; - ui.Rect? contentRect; - - ui.Picture build() { - final ui.Rect drawnRect = contentRect ?? ui.Rect.zero; - final ui.Rect rect = operation?.cullRect(drawnRect) ?? drawnRect; - final ui.PictureRecorder recorder = ui.PictureRecorder(); - final SceneCanvas canvas = ui.Canvas(recorder, rect) as SceneCanvas; - - operation?.pre(canvas, rect); - for (final PictureDrawCommand command in drawCommands) { - if (command.offset != ui.Offset.zero) { - canvas.save(); - canvas.translate(command.offset.dx, command.offset.dy); - canvas.drawPicture(command.picture); - canvas.restore(); - } else { - canvas.drawPicture(command.picture); - } - } - operation?.post(canvas, rect); - final ui.Picture picture = recorder.endRecording(); - layer?.picture = picture; - return picture; - } - - void addPicture( - ui.Offset offset, - ui.Picture picture, { - bool isComplexHint = false, - bool willChangeHint = false - }) { - drawCommands.add(PictureDrawCommand(offset, picture)); - final ui.Rect cullRect = (picture as ScenePicture).cullRect; - final ui.Rect shiftedRect = cullRect.shift(offset); - contentRect = contentRect?.expandToInclude(shiftedRect) ?? shiftedRect; - } -} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart index 6f0eb810948cf..fb16ad086f9f1 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_image.dart @@ -6,7 +6,9 @@ library skwasm_impl; import 'dart:ffi'; +import 'dart:js_interop'; +import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; final class RawImage extends Opaque {} @@ -38,17 +40,45 @@ external ImageHandle imageCreateFromPixels( int rowByteCount, ); -@Native(symbol: 'image_createFromVideoFrame', isLeaf: true) -external ImageHandle imageCreateFromVideoFrame( - int videoFrameId, +// We actually want this function to look something like this: +// +// @Native(symbol: 'image_createFromVideoFrame', isLeaf: true) +// external ImageHandle imageCreateFromVideoFrame( +// JSAny videoFrame, +// int width, +// int height, +// SurfaceHandle handle, +// ); +// +// However, this doesn't work because we cannot use extern refs as part of @Native +// annotations currently. For now, we can use JS interop to expose this function +// instead. +extension SkwasmImageExtension on SkwasmInstance { + @JS('wasmExports.image_createFromVideoFrame') + external JSNumber imageCreateFromVideoFrame( + VideoFrame frame, + JSNumber width, + JSNumber height, + JSNumber surfaceHandle, + ); +} +ImageHandle imageCreateFromVideoFrame( + VideoFrame frame, int width, int height, - SurfaceHandle handle, + SurfaceHandle handle +) => ImageHandle.fromAddress( + skwasmInstance.imageCreateFromVideoFrame( + frame, + width.toJS, + height.toJS, + handle.address.toJS, + ).toDartInt ); @Native(symbol:'image_ref', isLeaf: true) diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart index 9aa48f69e01f0..3831188c4df63 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/raw_surface.dart @@ -14,12 +14,10 @@ typedef SurfaceHandle = Pointer; final class RawRenderCallback extends Opaque {} typedef OnRenderCallbackHandle = Pointer; -@Native)>( - symbol: 'surface_createFromCanvas', - isLeaf: true) -external SurfaceHandle surfaceCreateFromCanvas( - Pointer querySelector -); +typedef CallbackId = int; + +@Native(symbol: 'surface_create', isLeaf: true) +external SurfaceHandle surfaceCreate(); @Native(symbol: 'surface_getThreadId', isLeaf: true) external int surfaceGetThreadId(SurfaceHandle handle); @@ -37,26 +35,17 @@ external void surfaceSetCallbackHandler( isLeaf: true) external void surfaceDestroy(SurfaceHandle surface); -@Native( - symbol: 'surface_setCanvasSize', - isLeaf: true) -external void surfaceSetCanvasSize( - SurfaceHandle surface, - int width, - int height -); - @Native( symbol: 'surface_renderPicture', isLeaf: true) -external int surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture); +external CallbackId surfaceRenderPicture(SurfaceHandle surface, PictureHandle picture); @Native(symbol: 'surface_rasterizeImage', isLeaf: true) -external int surfaceRasterizeImage( +external CallbackId surfaceRasterizeImage( SurfaceHandle handle, ImageHandle image, int format, diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart index 76cec92901e79..3063a557aaed1 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/raw/skwasm_module.dart @@ -17,23 +17,37 @@ extension WebAssemblyMemoryExtension on WebAssemblyMemory { class SkwasmInstance {} extension SkwasmInstanceExtension on SkwasmInstance { - external JSNumber addFunction(JSFunction function, JSString signature); - external void removeFunction(JSNumber functionPointer); - - @JS('skwasm_registerObject') - external void skwasmRegisterObject(JSNumber objectId, JSAny object); - - @JS('skwasm_unregisterObject') - external void skwasmUnregisterObject(JSNumber objectId); + external JSNumber getEmptyTableSlot(); - @JS('skwasm_getObject') - external JSAny skwasmGetObject(JSNumber objectId); + // The function here *must* be a directly exported wasm function, not a + // JavaScript function. If you actually need to add a JavaScript function, + // use `addFunction` instead. + external void setWasmTableEntry(JSNumber index, JSAny function); - @JS('skwasm_transferObjectToThread') - external void skwasmTransferObjectToThread(JSNumber objectId, JSNumber threadId); + external JSNumber addFunction(WebAssemblyFunction function); + external void removeFunction(JSNumber functionPointer); external WebAssemblyMemory get wasmMemory; } @JS('window._flutter_skwasmInstance') external SkwasmInstance get skwasmInstance; + +@JS() +@staticInterop +@anonymous +class WebAssemblyFunctionType { + external factory WebAssemblyFunctionType({ + required JSArray parameters, + required JSArray results, + }); +} + +@JS('WebAssembly.Function') +@staticInterop +class WebAssemblyFunction { + external factory WebAssemblyFunction( + WebAssemblyFunctionType functionType, + JSFunction function + ); +} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 08c86b269458e..ed0a2481d2f6c 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -13,9 +13,8 @@ import 'package:ui/ui.dart' as ui; import 'package:ui/ui_web/src/ui_web.dart' as ui_web; class SkwasmRenderer implements Renderer { - late DomCanvasElement sceneElement; late SkwasmSurface surface; - ui.Size? surfaceSize; + late EngineSceneView sceneView; @override final SkwasmFontCollection fontCollection = SkwasmFontCollection(); @@ -194,7 +193,7 @@ class SkwasmRenderer implements Renderer { ); @override - ui.SceneBuilder createSceneBuilder() => SkwasmSceneBuilder(); + ui.SceneBuilder createSceneBuilder() => EngineSceneBuilder(); @override ui.StrutStyle createStrutStyle({ @@ -348,14 +347,8 @@ class SkwasmRenderer implements Renderer { @override FutureOr initialize() { - // TODO(jacksongardner): This is very basic and doesn't work for element - // embedding or with platform views. We need to update this at some point - // to deal with those cases. - sceneElement = createDomCanvasElement(); - sceneElement.id = 'flt-scene'; - domDocument.body!.appendChild(sceneElement); - surface = SkwasmSurface('#flt-scene'); - domDocument.body!.removeChild(sceneElement); + surface = SkwasmSurface(); + sceneView = EngineSceneView(SkwasmPictureRenderer(surface)); } @override @@ -406,28 +399,14 @@ class SkwasmRenderer implements Renderer { } @override - Future renderScene(ui.Scene scene) async { - final ui.Size frameSize = ui.window.physicalSize; - if (frameSize != surfaceSize) { - final double logicalWidth = frameSize.width.ceil() / window.devicePixelRatio; - final double logicalHeight = frameSize.height.ceil() / window.devicePixelRatio; - final DomCSSStyleDeclaration style = sceneElement.style; - style.width = '${logicalWidth}px'; - style.height = '${logicalHeight}px'; - - surface.setSize(frameSize.width.ceil(), frameSize.height.ceil()); - surfaceSize = frameSize; - } - final SkwasmPicture picture = (scene as SkwasmScene).picture as SkwasmPicture; - await surface.renderPicture(picture); - } + Future renderScene(ui.Scene scene) => sceneView.renderScene(scene as EngineScene); @override String get rendererTag => 'skwasm'; @override void reset(FlutterViewEmbedder embedder) { - embedder.addSceneToSceneHost(sceneElement); + embedder.addSceneToSceneHost(sceneView.sceneElement); } static final Map> _programs = >{}; @@ -470,3 +449,13 @@ class SkwasmRenderer implements Renderer { lineNumber: lineNumber ); } + +class SkwasmPictureRenderer implements PictureRenderer { + SkwasmPictureRenderer(this.surface); + + SkwasmSurface surface; + + @override + FutureOr renderPicture(ScenePicture picture) => + surface.renderPicture(picture as SkwasmPicture); +} diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart index d4bd716de2b3c..bce7a8a62edf0 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/surface.dart @@ -7,14 +7,16 @@ import 'dart:ffi'; import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; + + class SkwasmSurface { - factory SkwasmSurface(String canvasQuerySelector) { + factory SkwasmSurface() { final SurfaceHandle surfaceHandle = withStackScope((StackScope scope) { - final Pointer pointer = scope.convertStringToNative(canvasQuerySelector); - return surfaceCreateFromCanvas(pointer); + return surfaceCreate(); }); final SkwasmSurface surface = SkwasmSurface._fromHandle(surfaceHandle); surface._initialize(); @@ -24,30 +26,32 @@ class SkwasmSurface { SkwasmSurface._fromHandle(this.handle) : threadId = surfaceGetThreadId(handle); final SurfaceHandle handle; OnRenderCallbackHandle _callbackHandle = nullptr; - final Map> _pendingCallbacks = >{}; + final Map> _pendingCallbacks = >{}; final int threadId; - int _currentObjectId = 0; - int acquireObjectId() => ++_currentObjectId; - void _initialize() { - _callbackHandle = - OnRenderCallbackHandle.fromAddress( - skwasmInstance.addFunction( - _callbackHandler.toJS, - 'vii'.toJS - ).toDartDouble.toInt() - ); + final WebAssemblyFunction wasmFunction = WebAssemblyFunction( + WebAssemblyFunctionType( + parameters: [ + 'i32'.toJS, + 'i32'.toJS, + 'externref'.toJS + ].toJS, + results: [].toJS + ), + _callbackHandler.toJS, + ); + _callbackHandle = OnRenderCallbackHandle.fromAddress( + skwasmInstance.addFunction(wasmFunction).toDartInt, + ); surfaceSetCallbackHandler(handle, _callbackHandle); } - void setSize(int width, int height) => - surfaceSetCanvasSize(handle, width, height); - - Future renderPicture(SkwasmPicture picture) { + Future renderPicture(SkwasmPicture picture) async { final int callbackId = surfaceRenderPicture(handle, picture.handle); - return _registerCallback(callbackId); + final DomImageBitmap bitmap = (await _registerCallback(callbackId)) as DomImageBitmap; + return bitmap; } Future rasterizeImage(SkwasmImage image, ui.ImageByteFormat format) async { @@ -56,7 +60,7 @@ class SkwasmSurface { image.handle, format.index, ); - final int context = await _registerCallback(callbackId); + final int context = (await _registerCallback(callbackId) as JSNumber).toDartInt; final SkDataHandle dataHandle = SkDataHandle.fromAddress(context); final int byteCount = skDataGetSize(dataHandle); final Pointer dataPointer = skDataGetConstPointer(dataHandle).cast(); @@ -68,16 +72,19 @@ class SkwasmSurface { return ByteData.sublistView(output); } - Future _registerCallback(int callbackId) { - final Completer completer = Completer(); + Future _registerCallback(int callbackId) { + final Completer completer = Completer(); _pendingCallbacks[callbackId] = completer; return completer.future; } - void _callbackHandler(JSNumber jsCallbackId, JSNumber jsPointer) { - final int callbackId = jsCallbackId.toDartDouble.toInt(); - final Completer completer = _pendingCallbacks.remove(callbackId)!; - completer.complete(jsPointer.toDartDouble.toInt()); + void _callbackHandler(JSNumber callbackId, JSNumber context, JSAny jsContext) { + final Completer completer = _pendingCallbacks.remove(callbackId.toDartInt)!; + if (jsContext.isUndefinedOrNull) { + completer.complete(context); + } else { + completer.complete(jsContext); + } } void dispose() { diff --git a/lib/web_ui/skwasm/BUILD.gn b/lib/web_ui/skwasm/BUILD.gn index 0cba9b2a49956..73f93813a0982 100644 --- a/lib/web_ui/skwasm/BUILD.gn +++ b/lib/web_ui/skwasm/BUILD.gn @@ -31,6 +31,11 @@ wasm_lib("skwasm") { "wrappers.h", ] + cflags = [ + "-mreference-types", + "-pthread", + ] + ldflags = [ "-std=c++20", "-lGL", @@ -40,10 +45,9 @@ wasm_lib("skwasm") { "-sPTHREAD_POOL_SIZE=1", "-sALLOW_MEMORY_GROWTH", "-sALLOW_TABLE_GROWTH", - "-sUSE_PTHREADS=1", "-lexports.js", "-sEXPORTED_FUNCTIONS=[stackAlloc]", - "-sEXPORTED_RUNTIME_METHODS=[addFunction,removeFunction,skwasm_registerObject,skwasm_unregisterObject,skwasm_getObject,skwasm_transferObjectToThread]", + "-sEXPORTED_RUNTIME_METHODS=[addFunction,wasmExports]", "-Wno-pthreads-mem-growth", "--js-library", rebase_path("library_skwasm_support.js"), @@ -57,6 +61,8 @@ wasm_lib("skwasm") { "-sASSERTIONS=1", "-sGL_ASSERTIONS=1", ] + } else { + ldflags += [ "--closure=1" ] } deps = [ diff --git a/lib/web_ui/skwasm/image.cpp b/lib/web_ui/skwasm/image.cpp index 291d4cd21e3f5..70553cfe98a9a 100644 --- a/lib/web_ui/skwasm/image.cpp +++ b/lib/web_ui/skwasm/image.cpp @@ -82,22 +82,17 @@ class ExternalWebGLTexture : public GrExternalTexture { class VideoFrameImageGenerator : public GrExternalTextureGenerator { public: VideoFrameImageGenerator(SkImageInfo ii, - SkwasmObjectId videoFrameId, + SkwasmObject videoFrame, Skwasm::Surface* surface) : GrExternalTextureGenerator(ii), - _videoFrameId(videoFrameId), - _surface(surface) {} - - ~VideoFrameImageGenerator() override { - _surface->disposeVideoFrame(_videoFrameId); - } + _videoFrameWrapper(surface->createVideoFrameWrapper(videoFrame)) {} std::unique_ptr generateExternalTexture( GrRecordingContext* context, GrMipMapped mipmapped) override { GrGLTextureInfo glInfo; glInfo.fID = skwasm_createGlTextureFromVideoFrame( - _videoFrameId, fInfo.width(), fInfo.height()); + _videoFrameWrapper->getVideoFrame(), fInfo.width(), fInfo.height()); glInfo.fFormat = GL_RGBA8_OES; glInfo.fTarget = GL_TEXTURE_2D; @@ -108,8 +103,7 @@ class VideoFrameImageGenerator : public GrExternalTextureGenerator { } private: - SkwasmObjectId _videoFrameId; - Skwasm::Surface* _surface; + std::unique_ptr _videoFrameWrapper; }; SKWASM_EXPORT SkImage* image_createFromPicture(SkPicture* picture, @@ -135,16 +129,17 @@ SKWASM_EXPORT SkImage* image_createFromPixels(SkData* data, .release(); } -SKWASM_EXPORT SkImage* image_createFromVideoFrame(SkwasmObjectId videoFrameId, +SKWASM_EXPORT SkImage* image_createFromVideoFrame(SkwasmObject videoFrame, int width, int height, Skwasm::Surface* surface) { return SkImages::DeferredFromTextureGenerator( - std::make_unique( - SkImageInfo::Make(width, height, - SkColorType::kRGBA_8888_SkColorType, - SkAlphaType::kPremul_SkAlphaType), - videoFrameId, surface)) + std::unique_ptr( + new VideoFrameImageGenerator( + SkImageInfo::Make(width, height, + SkColorType::kRGBA_8888_SkColorType, + SkAlphaType::kPremul_SkAlphaType), + videoFrame, surface))) .release(); } diff --git a/lib/web_ui/skwasm/library_skwasm_support.js b/lib/web_ui/skwasm/library_skwasm_support.js index 431b4e7da3581..87dfff43fdb94 100644 --- a/lib/web_ui/skwasm/library_skwasm_support.js +++ b/lib/web_ui/skwasm/library_skwasm_support.js @@ -8,44 +8,81 @@ mergeInto(LibraryManager.library, { $skwasm_support_setup__postset: 'skwasm_support_setup();', $skwasm_support_setup: function() { - const objectMap = new Map(); - skwasm_registerObject = function(id, object) { - objectMap.set(id, object); + const handleToCanvasMap = new Map(); + const associatedObjectsMap = new Map(); + _skwasm_setAssociatedObjectOnThread = function(threadId, pointer, object) { + PThread.pthreads[threadId].postMessage({ + skwasmMessage: 'setAssociatedObject', + pointer, + object, + }); }; - skwasm_unregisterObject = function(id) { - objectMap.delete(id); - } - skwasm_getObject = function(id) { - return objectMap.get(id); - } - - addEventListener('message', function (event) { - const transfers = event.data.skwasmObjectTransfers; - if (!transfers) { - return; + _skwasm_getAssociatedObject = function(pointer) { + return associatedObjectsMap.get(pointer); + }; + _skwasm_registerMessageListener = function(threadId) { + const eventListener = function({data}) { + const skwasmMessage = data.skwasmMessage; + if (!skwasmMessage) { + return; + } + switch (skwasmMessage) { + case 'onRenderComplete': + _surface_onRenderComplete(data.surface, data.callbackId, data.imageBitmap); + return; + case 'setAssociatedObject': + associatedObjectsMap.set(data.pointer, data.object); + return; + case 'disposeAssociatedObject': + const object = { data }; + if (object.close) { + object.close(); + } + associatedObjectsMap.delete(data.pointer); + default: + console.warn('unrecognized skwasm message'); + } + }; + if (!threadId) { + addEventListener("message", eventListener); + } else { + PThread.pthreads[threadId].addEventListener("message", eventListener); } - transfers.forEach(function(object, objectId) { - objectMap.set(objectId, object); - }); - }); - skwasm_transferObjectToMain = function(objectId) { + }; + _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; + }; + _skwasm_captureImageBitmap = async function(surfaceHandle, contextHandle, callbackId, width, height) { + const canvas = handleToCanvasMap.get(contextHandle); + const imageBitmap = await createImageBitmap(canvas, 0, 0, width, height); postMessage({ - skwasmObjectTransfers: new Map([ - [objectId, objectMap[objectId]] - ]) + skwasmMessage: 'onRenderComplete', + surface: surfaceHandle, + callbackId, + imageBitmap, }); - objectMap.delete(objectId); - } - skwasm_transferObjectToThread = function(objectId, threadId) { - PThread.pthreads[threadId].postMessage({ - skwasmObjectTransfers: new Map([ - [objectId, objectMap.get(objectId)] - ]) - }); - objectMap.delete(objectId); - } - _skwasm_createGlTextureFromVideoFrame = function(videoFrameId, width, height) { - const videoFrame = skwasm_getObject(videoFrameId); + }; + _skwasm_createGlTextureFromVideoFrame = function(videoFrame, width, height) { const glCtx = GL.currentContext.GLctx; const newTexture = glCtx.createTexture(); glCtx.bindTexture(glCtx.TEXTURE_2D, newTexture); @@ -59,26 +96,29 @@ mergeInto(LibraryManager.library, { const textureId = GL.getNewId(GL.textures); GL.textures[textureId] = newTexture; return textureId; - }, - _skwasm_disposeVideoFrame = function(videoFrameId) { - const videoFrame = skwasm_getObject(videoFrameId); - videoFrame.close(); - skwasm_unregisterObject(videoFrameId); - } + }; + _skwasm_disposeAssociatedObjectOnThread = function(threadId, object) { + PThread.pthreads[threadId].postMessage({ + skwasmMessage: 'disposeAssociatedObject', + object, + }); + }; }, - $skwasm_registerObject: function() {}, - $skwasm_registerObject__deps: ['$skwasm_support_setup'], - $skwasm_unregisterObject: function() {}, - $skwasm_unregisterObject__deps: ['$skwasm_support_setup'], - $skwasm_getObject: function() {}, - $skwasm_getObject__deps: ['$skwasm_support_setup'], - $skwasm_transferObjectToMain: function() {}, - $skwasm_transferObjectToMain__deps: ['$skwasm_support_setup'], - $skwasm_transferObjectToThread: function() {}, - $skwasm_transferObjectToThread__deps: ['$skwasm_support_setup'], + skwasm_setAssociatedObjectOnThread: function () {}, + skwasm_setAssociatedObjectOnThread__deps: ['$skwasm_support_setup'], + skwasm_getAssociatedObject: function () {}, + skwasm_getAssociatedObject__deps: ['$skwasm_support_setup'], + skwasm_disposeAssociatedObjectOnThread: function () {}, + skwasm_disposeAssociatedObjectOnThread__deps: ['$skwasm_support_setup'], + skwasm_registerMessageListener: function() {}, + skwasm_registerMessageListener__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 () {}, + skwasm_captureImageBitmap__deps: ['$skwasm_support_setup'], skwasm_createGlTextureFromVideoFrame: function () {}, - skwasm_createGlTextureFromVideoFrame__deps: ['$skwasm_support_setup', '$skwasm_getObject'], - skwasm_disposeVideoFrame: function () {}, - skwasm_disposeVideoFrame__deps: ['$skwasm_support_setup', '$skwasm_getObject', '$skwasm_unregisterObject'], + skwasm_createGlTextureFromVideoFrame__deps: ['$skwasm_support_setup'], }); \ No newline at end of file diff --git a/lib/web_ui/skwasm/skwasm_support.h b/lib/web_ui/skwasm/skwasm_support.h index 990f0de0963a6..9547bc29e7a89 100644 --- a/lib/web_ui/skwasm/skwasm_support.h +++ b/lib/web_ui/skwasm/skwasm_support.h @@ -5,15 +5,29 @@ #include #include -using SkwasmObjectId = uint32_t; +namespace Skwasm { +class Surface; +} + +using SkwasmObject = __externref_t; extern "C" { -extern void skwasm_transferObjectToMain(SkwasmObjectId objectId); -extern void skwasm_transferObjectToThread(SkwasmObjectId objectId, - pthread_t threadId); +extern void skwasm_setAssociatedObjectOnThread(unsigned long threadId, + void* pointer, + SkwasmObject object); +extern SkwasmObject skwasm_getAssociatedObject(void* pointer); +extern void skwasm_disposeAssociatedObjectOnThread(unsigned long threadId, + void* pointer); +extern void skwasm_registerMessageListener(pthread_t threadId); +extern uint32_t skwasm_createOffscreenCanvas(int width, int height); +extern void skwasm_resizeCanvas(uint32_t contextHandle, int width, int height); +extern void skwasm_captureImageBitmap(Skwasm::Surface* surfaceHandle, + uint32_t contextHandle, + uint32_t bitmapId, + int width, + int height); extern unsigned int skwasm_createGlTextureFromVideoFrame( - SkwasmObjectId videoFrameId, + SkwasmObject videoFrame, int width, int height); -extern void skwasm_disposeVideoFrame(SkwasmObjectId videoFrameId); } diff --git a/lib/web_ui/skwasm/surface.cpp b/lib/web_ui/skwasm/surface.cpp index f07257b92658d..6ea9da7be9d7a 100644 --- a/lib/web_ui/skwasm/surface.cpp +++ b/lib/web_ui/skwasm/surface.cpp @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "surface.h" +#include #include "third_party/skia/include/gpu/GrBackendSurface.h" #include "third_party/skia/include/gpu/GrDirectContext.h" @@ -10,13 +11,12 @@ using namespace Skwasm; -Surface::Surface(const char* canvasID) : _canvasID(canvasID) { +Surface::Surface() { assert(emscripten_is_main_browser_thread()); pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); - emscripten_pthread_attr_settransferredcanvases(&attr, _canvasID.c_str()); pthread_create( &_thread, &attr, @@ -25,6 +25,8 @@ Surface::Surface(const char* canvasID) : _canvasID(canvasID) { return nullptr; }, this); + // Listen to messages from the worker + skwasm_registerMessageListener(_thread); } // Main thread only @@ -35,30 +37,14 @@ void Surface::dispose() { this); } -// Main thread only -void Surface::setCanvasSize(int width, int height) { - assert(emscripten_is_main_browser_thread()); - emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VIII, - reinterpret_cast(fSetCanvasSize), - nullptr, this, width, height); -} - // Main thread only uint32_t Surface::renderPicture(SkPicture* picture) { assert(emscripten_is_main_browser_thread()); uint32_t callbackId = ++_currentCallbackId; picture->ref(); - emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VII, + emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VIII, reinterpret_cast(fRenderPicture), - nullptr, this, picture); - - // After drawing to the surface, the browser implicitly flushes the drawing - // commands at the end of the event loop. As a result, in order to make - // sure we call back after the rendering has actually occurred, we issue - // the callback in a subsequent event, after the flushing has happened. - emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VII, - reinterpret_cast(fNotifyRenderComplete), - nullptr, this, callbackId); + nullptr, this, picture, callbackId); return callbackId; } @@ -74,10 +60,10 @@ uint32_t Surface::rasterizeImage(SkImage* image, ImageByteFormat format) { return callbackId; } -void Surface::disposeVideoFrame(SkwasmObjectId videoFrameId) { - emscripten_dispatch_to_thread(_thread, EM_FUNC_SIG_VII, - reinterpret_cast(fDisposeVideoFrame), - nullptr, this, videoFrameId); +std::unique_ptr Surface::createVideoFrameWrapper( + SkwasmObject videoFrame) { + return std::unique_ptr( + new VideoFrameWrapper(_thread, videoFrame)); } // Main thread only @@ -89,34 +75,21 @@ void Surface::setCallbackHandler(CallbackHandler* callbackHandler) { // Worker thread only void Surface::_runWorker() { _init(); - emscripten_unwind_to_js_event_loop(); + emscripten_exit_with_live_runtime(); } // Worker thread only void Surface::_init() { - EmscriptenWebGLContextAttributes attributes; - emscripten_webgl_init_context_attributes(&attributes); - - attributes.alpha = true; - attributes.depth = true; - attributes.stencil = true; - attributes.antialias = false; - attributes.premultipliedAlpha = true; - attributes.preserveDrawingBuffer = 0; - attributes.powerPreference = EM_WEBGL_POWER_PREFERENCE_DEFAULT; - attributes.failIfMajorPerformanceCaveat = false; - attributes.enableExtensionsByDefault = true; - attributes.explicitSwapControl = false; - attributes.renderViaOffscreenBackBuffer = true; - attributes.majorVersion = 2; - - _glContext = emscripten_webgl_create_context(_canvasID.c_str(), &attributes); + // Listen to messages from the main thread + skwasm_registerMessageListener(0); + _glContext = skwasm_createOffscreenCanvas(256, 256); if (!_glContext) { printf("Failed to create context!\n"); return; } makeCurrent(_glContext); + emscripten_webgl_enable_extension(_glContext, "WEBGL_debug_renderer_info"); _grContext = GrDirectContext::MakeGL(GrGLMakeNativeInterface()); @@ -144,11 +117,10 @@ void Surface::_dispose() { } // Worker thread only -void Surface::_setCanvasSize(int width, int height) { - if (_canvasWidth != width || _canvasHeight != height) { - emscripten_set_canvas_element_size(_canvasID.c_str(), width, height); - _canvasWidth = width; - _canvasHeight = height; +void Surface::_resizeCanvasToFit(int width, int height) { + if (!_surface || width > _canvasWidth || height > _canvasHeight) { + _canvasWidth = std::max(width, _canvasWidth); + _canvasHeight = std::max(height, _canvasHeight); _recreateSurface(); } } @@ -156,6 +128,7 @@ void Surface::_setCanvasSize(int width, int height) { // Worker thread only void Surface::_recreateSurface() { makeCurrent(_glContext); + skwasm_resizeCanvas(_glContext, _canvasWidth, _canvasHeight); auto target = GrBackendRenderTargets::MakeGL(_canvasWidth, _canvasHeight, _sampleCount, _stencil, _fbInfo); _surface = SkSurfaces::WrapBackendRenderTarget( @@ -164,16 +137,20 @@ void Surface::_recreateSurface() { } // Worker thread only -void Surface::_renderPicture(const SkPicture* picture) { - if (!_surface) { - printf("Can't render picture with no surface.\n"); - return; - } - +void Surface::_renderPicture(const SkPicture* picture, uint32_t callbackId) { + SkRect pictureRect = picture->cullRect(); + SkIRect roundedOutRect; + pictureRect.roundOut(&roundedOutRect); + _resizeCanvasToFit(roundedOutRect.width(), roundedOutRect.height()); + SkMatrix matrix = + SkMatrix::Translate(-roundedOutRect.fLeft, -roundedOutRect.fTop); makeCurrent(_glContext); auto canvas = _surface->getCanvas(); - canvas->drawPicture(picture); + canvas->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc); + canvas->drawPicture(sk_ref_sp(picture), &matrix, nullptr); _grContext->flush(_surface); + skwasm_captureImageBitmap(this, _glContext, callbackId, + roundedOutRect.width(), roundedOutRect.height()); } void Surface::_rasterizeImage(SkImage* image, @@ -200,51 +177,31 @@ void Surface::_rasterizeImage(SkImage* image, data = nullptr; } } - emscripten_sync_run_in_main_runtime_thread( + emscripten_async_run_in_main_runtime_thread( EM_FUNC_SIG_VIII, fOnRasterizeComplete, this, data.release(), callbackId); } -void Surface::_disposeVideoFrame(SkwasmObjectId objectId) { - skwasm_disposeVideoFrame(objectId); -} - void Surface::_onRasterizeComplete(SkData* data, uint32_t callbackId) { - _callbackHandler(callbackId, data); -} - -// Worker thread only -void Surface::_notifyRenderComplete(uint32_t callbackId) { - emscripten_sync_run_in_main_runtime_thread(EM_FUNC_SIG_VII, fOnRenderComplete, - this, callbackId); + _callbackHandler(callbackId, data, __builtin_wasm_ref_null_extern()); } // Main thread only -void Surface::_onRenderComplete(uint32_t callbackId) { +void Surface::onRenderComplete(uint32_t callbackId, SkwasmObject imageBitmap) { assert(emscripten_is_main_browser_thread()); - _callbackHandler(callbackId, nullptr); + _callbackHandler(callbackId, nullptr, imageBitmap); } void Surface::fDispose(Surface* surface) { surface->_dispose(); } -void Surface::fSetCanvasSize(Surface* surface, int width, int height) { - surface->_setCanvasSize(width, height); -} - -void Surface::fRenderPicture(Surface* surface, SkPicture* picture) { - surface->_renderPicture(picture); +void Surface::fRenderPicture(Surface* surface, + SkPicture* picture, + uint32_t callbackId) { + surface->_renderPicture(picture, callbackId); picture->unref(); } -void Surface::fNotifyRenderComplete(Surface* surface, uint32_t callbackId) { - surface->_notifyRenderComplete(callbackId); -} - -void Surface::fOnRenderComplete(Surface* surface, uint32_t callbackId) { - surface->_onRenderComplete(callbackId); -} - void Surface::fOnRasterizeComplete(Surface* surface, SkData* imageData, uint32_t callbackId) { @@ -259,13 +216,8 @@ void Surface::fRasterizeImage(Surface* surface, image->unref(); } -void Surface::fDisposeVideoFrame(Surface* surface, - SkwasmObjectId videoFrameId) { - surface->_disposeVideoFrame(videoFrameId); -} - -SKWASM_EXPORT Surface* surface_createFromCanvas(const char* canvasID) { - return new Surface(canvasID); +SKWASM_EXPORT Surface* surface_create() { + return new Surface(); } SKWASM_EXPORT unsigned long surface_getThreadId(Surface* surface) { @@ -282,12 +234,6 @@ SKWASM_EXPORT void surface_destroy(Surface* surface) { surface->dispose(); } -SKWASM_EXPORT void surface_setCanvasSize(Surface* surface, - int width, - int height) { - surface->setCanvasSize(width, height); -} - SKWASM_EXPORT uint32_t surface_renderPicture(Surface* surface, SkPicture* picture) { return surface->renderPicture(picture); @@ -298,3 +244,11 @@ SKWASM_EXPORT uint32_t surface_rasterizeImage(Surface* surface, ImageByteFormat format) { return surface->rasterizeImage(image, format); } + +// This is used by the skwasm JS support code to call back into C++ when the +// we finish creating the image bitmap, which is an asynchronous operation. +SKWASM_EXPORT void surface_onRenderComplete(Surface* surface, + uint32_t callbackId, + SkwasmObject imageBitmap) { + return surface->onRenderComplete(callbackId, imageBitmap); +} diff --git a/lib/web_ui/skwasm/surface.h b/lib/web_ui/skwasm/surface.h index 3fc6d8edcf3c8..22cc57be9876c 100644 --- a/lib/web_ui/skwasm/surface.h +++ b/lib/web_ui/skwasm/surface.h @@ -33,40 +33,54 @@ enum class ImageByteFormat { png, }; -class Surface { +class VideoFrameWrapper { public: + VideoFrameWrapper(unsigned long threadId, SkwasmObject videoFrame) + : _rasterThreadId(threadId) { + skwasm_setAssociatedObjectOnThread(_rasterThreadId, this, videoFrame); + } + + ~VideoFrameWrapper() { + skwasm_disposeAssociatedObjectOnThread(_rasterThreadId, this); + } + + SkwasmObject getVideoFrame() { return skwasm_getAssociatedObject(this); } + + private: + unsigned long _rasterThreadId; +}; + +class Surface { public: - using CallbackHandler = void(uint32_t, void*); + using CallbackHandler = void(uint32_t, void*, SkwasmObject); // Main thread only - Surface(const char* canvasID); + Surface(); unsigned long getThreadId() { return _thread; } // Main thread only void dispose(); - void setCanvasSize(int width, int height); uint32_t renderPicture(SkPicture* picture); uint32_t rasterizeImage(SkImage* image, ImageByteFormat format); void setCallbackHandler(CallbackHandler* callbackHandler); + void onRenderComplete(uint32_t callbackId, SkwasmObject imageBitmap); // Any thread - void disposeVideoFrame(SkwasmObjectId videoFrameId); + std::unique_ptr createVideoFrameWrapper( + SkwasmObject videoFrame); private: void _runWorker(); void _init(); void _dispose(); - void _setCanvasSize(int width, int height); + void _resizeCanvasToFit(int width, int height); void _recreateSurface(); - void _renderPicture(const SkPicture* picture); + void _renderPicture(const SkPicture* picture, uint32_t callbackId); void _rasterizeImage(SkImage* image, ImageByteFormat format, uint32_t callbackId); - void _disposeVideoFrame(SkwasmObjectId objectId); void _onRasterizeComplete(SkData* data, uint32_t callbackId); - void _notifyRenderComplete(uint32_t callbackId); - void _onRenderComplete(uint32_t callbackId); std::string _canvasID; CallbackHandler* _callbackHandler = nullptr; @@ -85,10 +99,12 @@ class Surface { pthread_t _thread; static void fDispose(Surface* surface); - static void fSetCanvasSize(Surface* surface, int width, int height); - static void fRenderPicture(Surface* surface, SkPicture* picture); - static void fNotifyRenderComplete(Surface* surface, uint32_t callbackId); - static void fOnRenderComplete(Surface* surface, uint32_t callbackId); + static void fRenderPicture(Surface* surface, + SkPicture* picture, + uint32_t callbackId); + static void fOnRenderComplete(Surface* surface, + uint32_t callbackId, + SkwasmObject imageBitmap); static void fRasterizeImage(Surface* surface, SkImage* image, ImageByteFormat format, @@ -96,6 +112,5 @@ class Surface { static void fOnRasterizeComplete(Surface* surface, SkData* imageData, uint32_t callbackId); - static void fDisposeVideoFrame(Surface* surface, SkwasmObjectId videoFrameId); }; } // namespace Skwasm diff --git a/lib/web_ui/test/engine/scene_builder_test.dart b/lib/web_ui/test/engine/scene_builder_test.dart new file mode 100644 index 0000000000000..693f39203dad0 --- /dev/null +++ b/lib/web_ui/test/engine/scene_builder_test.dart @@ -0,0 +1,409 @@ +// 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: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; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + setUpAll(() { + LayerBuilder.debugRecorderFactory = (ui.Rect rect) { + final StubSceneCanvas canvas = StubSceneCanvas(); + final StubPictureRecorder recorder = StubPictureRecorder(canvas); + return (recorder, canvas); + }; + }); + + tearDownAll(() { + LayerBuilder.debugRecorderFactory = null; + }); + + group('EngineSceneBuilder', () { + test('single picture', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + expect(slices.length, 1); + expect(slices[0], pictureSliceWithRect(pictureRect)); + }); + + test('two pictures', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(300, 400, 400, 400); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + expect(slices.length, 1); + expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(100, 100, 400, 400))); + }); + + test('picture + platform view (overlapping)', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); + const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); + sceneBuilder.addPlatformView( + 1, + offset: platformViewRect.topLeft, + width: platformViewRect.width, + height: platformViewRect.height + ); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], pictureSliceWithRect(pictureRect)); + expect(slices[1], platformViewSliceWithViews([ + PlatformView(1, platformViewRect.size, PlatformViewStyling( + position: PlatformViewPosition.offset(platformViewRect.topLeft) + )) + ])); + }); + + test('platform view + picture (overlapping)', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect = ui.Rect.fromLTRB(100, 100, 200, 200); + const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); + sceneBuilder.addPlatformView( + 1, + offset: platformViewRect.topLeft, + width: platformViewRect.width, + height: platformViewRect.height + ); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect)); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + expect(slices.length, 2); + expect(slices[0], platformViewSliceWithViews([ + PlatformView(1, platformViewRect.size, PlatformViewStyling( + position: PlatformViewPosition.offset(platformViewRect.topLeft) + )) + ])); + expect(slices[1], pictureSliceWithRect(pictureRect)); + }); + + test('platform view sandwich (overlapping)', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); + const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(200, 200, 300, 300); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); + sceneBuilder.addPlatformView( + 1, + offset: platformViewRect.topLeft, + width: platformViewRect.width, + height: platformViewRect.height + ); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + expect(slices.length, 3); + expect(slices[0], pictureSliceWithRect(pictureRect1)); + expect(slices[1], platformViewSliceWithViews([ + PlatformView(1, platformViewRect.size, PlatformViewStyling( + position: PlatformViewPosition.offset(platformViewRect.topLeft) + )) + ])); + expect(slices[2], pictureSliceWithRect(pictureRect2)); + }); + + test('platform view sandwich (non-overlapping)', () { + final EngineSceneBuilder sceneBuilder = EngineSceneBuilder(); + + const ui.Rect pictureRect1 = ui.Rect.fromLTRB(100, 100, 200, 200); + const ui.Rect platformViewRect = ui.Rect.fromLTRB(150, 150, 250, 250); + const ui.Rect pictureRect2 = ui.Rect.fromLTRB(50, 50, 100, 100); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect1)); + sceneBuilder.addPlatformView( + 1, + offset: platformViewRect.topLeft, + width: platformViewRect.width, + height: platformViewRect.height + ); + sceneBuilder.addPicture(ui.Offset.zero, StubPicture(pictureRect2)); + + final EngineScene scene = sceneBuilder.build() as EngineScene; + final List slices = scene.rootLayer.slices; + + // The top picture does not overlap with the platform view, so it should + // be grouped into the slice below it to reduce the number of canvases we + // need. + expect(slices.length, 2); + expect(slices[0], pictureSliceWithRect(const ui.Rect.fromLTRB(50, 50, 200, 200))); + expect(slices[1], platformViewSliceWithViews([ + PlatformView(1, platformViewRect.size, PlatformViewStyling( + position: PlatformViewPosition.offset(platformViewRect.topLeft) + )) + ])); + }); + }); +} + +PictureSliceMatcher pictureSliceWithRect(ui.Rect rect) => PictureSliceMatcher(rect); +PlatformViewSliceMatcher platformViewSliceWithViews(List views) + => PlatformViewSliceMatcher(views); + +class PictureSliceMatcher extends Matcher { + PictureSliceMatcher(this.expectedRect); + + final ui.Rect expectedRect; + + @override + Description describe(Description description) { + return description.add(''); + } + + @override + bool matches(dynamic item, Map matchState) { + if (item is! PictureSlice) { + return false; + } + final ScenePicture picture = item.picture; + if (picture is! StubPicture) { + return false; + } + + if (picture.cullRect != expectedRect) { + return false; + } + + return true; + } +} + +class PlatformViewSliceMatcher extends Matcher { + PlatformViewSliceMatcher(this.expectedPlatformViews); + + final List expectedPlatformViews; + + @override + Description describe(Description description) { + return description.add(''); + } + + @override + bool matches(dynamic item, Map matchState) { + if (item is! PlatformViewSlice) { + return false; + } + + if (item.views.length != expectedPlatformViews.length) { + return false; + } + + for (int i = 0; i < item.views.length; i++) { + final PlatformView expectedView = expectedPlatformViews[i]; + final PlatformView actualView = item.views[i]; + if (expectedView.viewId != actualView.viewId) { + return false; + } + if (expectedView.size != actualView.size) { + return false; + } + if (expectedView.styling != actualView.styling) { + return false; + } + } + return true; + } +} + +class StubPicture implements ScenePicture { + StubPicture(this.cullRect); + + @override + final ui.Rect cullRect; + + @override + int get approximateBytesUsed => throw UnimplementedError(); + + @override + bool get debugDisposed => throw UnimplementedError(); + + @override + void dispose() {} + + @override + Future toImage(int width, int height) { + throw UnimplementedError(); + } + + @override + ui.Image toImageSync(int width, int height) { + throw UnimplementedError(); + } +} + +class StubCompositePicture extends StubPicture { + StubCompositePicture(this.children) : super( + children.fold(null, (ui.Rect? previousValue, StubPicture child) { + return previousValue?.expandToInclude(child.cullRect) ?? child.cullRect; + })! + ); + + final List children; +} + +class StubPictureRecorder implements ui.PictureRecorder { + StubPictureRecorder(this.canvas); + + final StubSceneCanvas canvas; + + @override + ui.Picture endRecording() { + return StubCompositePicture(canvas.pictures); + } + + @override + bool get isRecording => throw UnimplementedError(); +} + +class StubSceneCanvas implements SceneCanvas { + List pictures = []; + + @override + void drawPicture(ui.Picture picture) { + pictures.add(picture as StubPicture); + } + + @override + void clipPath(ui.Path path, {bool doAntiAlias = true}) {} + + @override + void clipRRect(ui.RRect rrect, {bool doAntiAlias = true}) {} + + @override + void clipRect(ui.Rect rect, {ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true}) {} + + @override + void drawArc(ui.Rect rect, double startAngle, double sweepAngle, bool useCenter, ui.Paint paint) {} + + @override + void drawAtlas(ui.Image atlas, List transforms, List rects, List? colors, ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) {} + + @override + void drawCircle(ui.Offset c, double radius, ui.Paint paint) {} + + @override + void drawColor(ui.Color color, ui.BlendMode blendMode) {} + + @override + void drawDRRect(ui.RRect outer, ui.RRect inner, ui.Paint paint) {} + + @override + void drawImage(ui.Image image, ui.Offset offset, ui.Paint paint) {} + + @override + void drawImageNine(ui.Image image, ui.Rect center, ui.Rect dst, ui.Paint paint) {} + + @override + void drawImageRect(ui.Image image, ui.Rect src, ui.Rect dst, ui.Paint paint) {} + + @override + void drawLine(ui.Offset p1, ui.Offset p2, ui.Paint paint) {} + + @override + void drawOval(ui.Rect rect, ui.Paint paint) {} + + @override + void drawPaint(ui.Paint paint) {} + + @override + void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {} + + @override + void drawPath(ui.Path path, ui.Paint paint) {} + + @override + void drawPoints(ui.PointMode pointMode, List points, ui.Paint paint) {} + + @override + void drawRRect(ui.RRect rrect, ui.Paint paint) {} + + @override + void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List? colors, ui.BlendMode? blendMode, ui.Rect? cullRect, ui.Paint paint) {} + + @override + void drawRawPoints(ui.PointMode pointMode, Float32List points, ui.Paint paint) {} + + @override + void drawRect(ui.Rect rect, ui.Paint paint) {} + + @override + void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) {} + + @override + void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, ui.Paint paint) {} + + @override + ui.Rect getDestinationClipBounds() { + throw UnimplementedError(); + } + + @override + ui.Rect getLocalClipBounds() { + throw UnimplementedError(); + } + + @override + int getSaveCount() { + throw UnimplementedError(); + } + + @override + Float64List getTransform() { + throw UnimplementedError(); + } + + @override + void restore() {} + + @override + void restoreToCount(int count) {} + + @override + void rotate(double radians) {} + + @override + void save() {} + + @override + void saveLayer(ui.Rect? bounds, ui.Paint paint) {} + + @override + void saveLayerWithFilter(ui.Rect? bounds, ui.Paint paint, ui.ImageFilter backdropFilter) {} + + @override + void scale(double sx, [double? sy]) {} + + @override + void skew(double sx, double sy) {} + + @override + void transform(Float64List matrix4) {} + + @override + void translate(double dx, double dy) {} +} diff --git a/lib/web_ui/test/ui/draw_atlas_golden_test.dart b/lib/web_ui/test/ui/draw_atlas_golden_test.dart index 992799ad1ca27..05870eafe2873 100644 --- a/lib/web_ui/test/ui/draw_atlas_golden_test.dart +++ b/lib/web_ui/test/ui/draw_atlas_golden_test.dart @@ -116,6 +116,16 @@ Future testMain() async { ); const ui.Rect region = ui.Rect.fromLTWH(0, 0, 300, 300); + + test('render atlas', () async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder, region); + final ui.Image atlas = generateAtlas(); + canvas.drawImage(atlas, ui.Offset.zero, ui.Paint()); + await drawPictureUsingCurrentRenderer(recorder.endRecording()); + await matchGoldenFile('ui_atlas.png', region: region); + }, skip: isHtml); // HTML renderer doesn't support drawAtlas + test('drawAtlas', () async { final ui.PictureRecorder recorder = ui.PictureRecorder(); final ui.Canvas canvas = ui.Canvas(recorder, region); diff --git a/lib/web_ui/test/ui/platform_view_test.dart b/lib/web_ui/test/ui/platform_view_test.dart new file mode 100644 index 0000000000000..7c934caf6087b --- /dev/null +++ b/lib/web_ui/test/ui/platform_view_test.dart @@ -0,0 +1,179 @@ +// 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 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; +import 'package:ui/ui_web/src/ui_web.dart' as ui_web; +import 'package:web_engine_tester/golden_tester.dart'; + +import '../common/test_initialization.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + setUpUnitTests( + emulateTesterEnvironment: false, + setUpTestViewDimensions: false, + ); + + const ui.Rect region = ui.Rect.fromLTWH(0, 0, 300, 300); + const String platformViewType = 'test-platform-view'; + + setUp(() { + ui_web.platformViewRegistry.registerViewFactory( + platformViewType, + (int viewId) { + final DomElement element = createDomHTMLDivElement(); + element.style.backgroundColor = 'blue'; + return element; + } + ); + }); + + tearDown(() { + platformViewManager.debugClear(); + }); + + test('picture + overlapping platformView', () async { + await _createPlatformView(1, platformViewType); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawCircle( + const ui.Offset(50, 50), + 50, + ui.Paint() + ..style = ui.PaintingStyle.fill + ..color = const ui.Color(0xFFFF0000) + ); + + final ui.SceneBuilder sb = ui.SceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(const ui.Offset(100, 100), recorder.endRecording()); + + sb.addPlatformView( + 1, + offset: const ui.Offset(125, 125), + width: 50, + height: 50, + ); + await renderer.renderScene(sb.build()); + + await matchGoldenFile('picture_platformview_overlap.png', region: region); + }); + + test('platformView sandwich', () async { + await _createPlatformView(1, platformViewType); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawCircle( + const ui.Offset(50, 50), + 50, + ui.Paint() + ..style = ui.PaintingStyle.fill + ..color = const ui.Color(0xFF00FF00) + ); + + final ui.Picture picture = recorder.endRecording(); + + final ui.SceneBuilder sb = ui.SceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(const ui.Offset(75, 75), picture); + + sb.addPlatformView( + 1, + offset: const ui.Offset(100, 100), + width: 100, + height: 100, + ); + + sb.addPicture(const ui.Offset(125, 125), picture); + await renderer.renderScene(sb.build()); + + await matchGoldenFile('picture_platformview_sandwich.png', region: region); + }); + + test('transformed platformview', () async { + await _createPlatformView(1, platformViewType); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawCircle( + const ui.Offset(50, 50), + 50, + ui.Paint() + ..style = ui.PaintingStyle.fill + ..color = const ui.Color(0xFFFF0000) + ); + + final ui.SceneBuilder sb = ui.SceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(const ui.Offset(100, 100), recorder.endRecording()); + + sb.pushTransform(Matrix4.rotationZ(0.1).toFloat64()); + sb.addPlatformView( + 1, + offset: const ui.Offset(125, 125), + width: 50, + height: 50, + ); + await renderer.renderScene(sb.build()); + + await matchGoldenFile('platformview_transformed.png', region: region); + }); + + test('platformview with opacity', () async { + await _createPlatformView(1, platformViewType); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + canvas.drawCircle( + const ui.Offset(50, 50), + 50, + ui.Paint() + ..style = ui.PaintingStyle.fill + ..color = const ui.Color(0xFFFF0000) + ); + + final ui.SceneBuilder sb = ui.SceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(const ui.Offset(100, 100), recorder.endRecording()); + + sb.pushOpacity(127); + sb.addPlatformView( + 1, + offset: const ui.Offset(125, 125), + width: 50, + height: 50, + ); + await renderer.renderScene(sb.build()); + + await matchGoldenFile('platformview_opacity.png', region: region); + }); +} + +// Sends a platform message to create a Platform View with the given id and viewType. +Future _createPlatformView(int id, String viewType) { + final Completer completer = Completer(); + const MethodCodec codec = StandardMethodCodec(); + window.sendPlatformMessage( + 'flutter/platform_views', + codec.encodeMethodCall(MethodCall( + 'create', + { + 'id': id, + 'viewType': viewType, + }, + )), + (dynamic _) => completer.complete(), + ); + return completer.future; +} diff --git a/tools/activate_emsdk.py b/tools/activate_emsdk.py index 9b0d11dbf740d..3653bc3eb9700 100644 --- a/tools/activate_emsdk.py +++ b/tools/activate_emsdk.py @@ -16,7 +16,7 @@ EMSDK_PATH = os.path.join(EMSDK_ROOT, 'emsdk.py') # See lib/web_ui/README.md for instructions on updating the EMSDK version. -EMSDK_VERSION = '3.1.32' +EMSDK_VERSION = '3.1.44' def main():