diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index d8405fe3ba..2160530656 100755 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -493,6 +493,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/color_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/image_filter.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/offset.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/opacity.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/painting.dart @@ -518,6 +519,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/normalized_gradien FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/vertex_shaders.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index c2f69676f6..7431a9799d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -41,6 +41,9 @@ export 'engine/browser_detection.dart'; import 'engine/html_image_codec.dart'; export 'engine/html_image_codec.dart'; +import 'engine/html/offscreen_canvas.dart'; +export 'engine/html/offscreen_canvas.dart'; + import 'engine/html/painting.dart'; export 'engine/html/painting.dart'; @@ -83,6 +86,9 @@ export 'engine/html/shaders/shader_builder.dart'; import 'engine/html/shaders/vertex_shaders.dart'; export 'engine/html/shaders/vertex_shaders.dart'; +import 'engine/html/shaders/webgl_context.dart'; +export 'engine/html/shaders/webgl_context.dart'; + import 'engine/mouse_cursor.dart'; export 'engine/mouse_cursor.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart new file mode 100644 index 0000000000..d1c113da46 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:js_util' as js_util; +import 'package:ui/src/engine.dart'; + +/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. +class OffScreenCanvas { + html.OffscreenCanvas? offScreenCanvas; + html.CanvasElement? canvasElement; + int width; + int height; + static bool? _supported; + + OffScreenCanvas(this.width, this.height) { + if (OffScreenCanvas.supported) { + offScreenCanvas = html.OffscreenCanvas(width, height); + } else { + canvasElement = html.CanvasElement( + width: width, + height: height, + ); + canvasElement!.className = 'gl-canvas'; + final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio; + final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio; + canvasElement!.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; + } + } + + void dispose() { + offScreenCanvas = null; + canvasElement = null; + } + + /// Returns CanvasRenderContext2D or OffscreenCanvasRenderingContext2D to + /// paint into. + Object? getContext2d() { + return (offScreenCanvas != null + ? offScreenCanvas!.getContext('2d') + : canvasElement!.getContext('2d')); + } + + /// Feature detection for transferToImageBitmap on OffscreenCanvas. + bool get transferToImageBitmapSupported => + js_util.hasProperty(offScreenCanvas!, 'transferToImageBitmap'); + + /// Creates an ImageBitmap object from the most recently rendered image + /// of the OffscreenCanvas. + /// + /// !Warning API still in experimental status, feature detect before using. + Object? transferToImageBitmap() { + return js_util.callMethod(offScreenCanvas!, 'transferToImageBitmap', + []); + } + + /// Draws canvas contents to a rendering context. + void transferImage(Object targetContext) { + // Actual size of canvas may be larger than viewport size. Use + // source/destination to draw part of the image data. + js_util.callMethod(targetContext, 'drawImage', + [offScreenCanvas ?? canvasElement!, 0, 0, width, height, + 0, 0, width, height]); + } + + /// Converts canvas contents to an image and returns as data url. + Future toDataUrl() { + final Completer completer = Completer(); + if (offScreenCanvas != null) { + offScreenCanvas!.convertToBlob().then((html.Blob value) { + final fileReader = html.FileReader(); + fileReader.onLoad.listen((event) { + completer.complete(js_util.getProperty( + js_util.getProperty(event, 'target')!, 'result')!); + }); + fileReader.readAsDataUrl(value); + }); + return completer.future; + } else { + return Future.value(canvasElement!.toDataUrl()); + } + } + + /// Draws an image to canvas for both offscreen canvas canvas context2d. + void drawImage(Object image, int x, int y, int width, int height) { + js_util.callMethod( + getContext2d()!, 'drawImage', [image, x, y, width, height]); + } + + /// Feature detects OffscreenCanvas. + static bool get supported => _supported ??= + js_util.hasProperty(html.window, 'OffscreenCanvas'); +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart index 7e4acb3564..8474d949fd 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -58,7 +58,7 @@ void initWebGl() { } void disposeWebGl() { - _GlContextCache.dispose(); + GlContextCache.dispose(); _glRenderer = null; } @@ -180,7 +180,7 @@ class _WebGlRenderer implements _GlRenderer { : _writeVerticesTextureFragmentShader(isWebGl2, imageShader.tileModeX, imageShader.tileModeY); - GlContext gl = _GlContextCache.createGlContext(widthInPixels, heightInPixels)!; + GlContext gl = GlContextCache.createGlContext(widthInPixels, heightInPixels)!; GlProgram glProgram = gl.cacheProgram(vertexShader, fragmentShader); gl.useProgram(glProgram); @@ -613,512 +613,3 @@ Float32List _convertVertexPositions(ui.VertexMode mode, Float32List positions) { return triangleList; } } - -/// Compiled and cached gl program. -class GlProgram { - final Object program; - GlProgram(this.program); -} - -/// JS Interop helper for webgl apis. -class GlContext { - final Object glContext; - final bool isOffscreen; - dynamic _kCompileStatus; - dynamic _kArrayBuffer; - dynamic _kElementArrayBuffer; - dynamic _kStaticDraw; - dynamic _kFloat; - dynamic _kColorBufferBit; - dynamic _kTexture2D; - dynamic _kTextureWrapS; - dynamic _kTextureWrapT; - dynamic _kRepeat; - dynamic _kClampToEdge; - dynamic _kMirroredRepeat; - dynamic _kTriangles; - dynamic _kLinkStatus; - dynamic _kUnsignedByte; - dynamic _kUnsignedShort; - dynamic _kRGBA; - dynamic _kLinear; - dynamic _kTextureMinFilter; - int? _kTexture0; - - Object? _canvas; - int? _widthInPixels; - int? _heightInPixels; - static late Map _programCache; - - GlContext.fromOffscreenCanvas(html.OffscreenCanvas canvas) - : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, - isOffscreen = true { - _programCache = {}; - _canvas = canvas; - } - - GlContext.fromCanvas(html.CanvasElement canvas, bool useWebGl1) - : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', - {'premultipliedAlpha': false})!, - isOffscreen = false { - _programCache = {}; - _canvas = canvas; - } - - void setViewportSize(int width, int height) { - _widthInPixels = width; - _heightInPixels = height; - } - - /// Draws Gl context contents to canvas context. - void drawImage(html.CanvasRenderingContext2D context, - double left, double top) { - // Actual size of canvas may be larger than viewport size. Use - // source/destination to draw part of the image data. - js_util.callMethod(context, 'drawImage', - [_canvas, 0, 0, _widthInPixels, _heightInPixels, - left, top, _widthInPixels, _heightInPixels]); - } - - GlProgram cacheProgram( - String vertexShaderSource, String fragmentShaderSource) { - String cacheKey = '$vertexShaderSource||$fragmentShaderSource'; - GlProgram? cachedProgram = _programCache[cacheKey]; - if (cachedProgram == null) { - // Create and compile shaders. - Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); - Object fragmentShader = - compileShader('FRAGMENT_SHADER', fragmentShaderSource); - // Create a gl program and link shaders. - Object program = createProgram(); - attachShader(program, vertexShader); - attachShader(program, fragmentShader); - linkProgram(program); - cachedProgram = GlProgram(program); - _programCache[cacheKey] = cachedProgram; - } - return cachedProgram; - } - - Object compileShader(String shaderType, String source) { - Object? shader = _createShader(shaderType); - if (shader == null) { - throw Exception(error); - } - js_util.callMethod(glContext, 'shaderSource', [shader, source]); - js_util.callMethod(glContext, 'compileShader', [shader]); - bool shaderStatus = js_util - .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); - if (!shaderStatus) { - throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); - } - return shader; - } - - Object createProgram() => - js_util.callMethod(glContext, 'createProgram', const [])!; - - void attachShader(Object? program, Object shader) { - js_util.callMethod(glContext, 'attachShader', [program, shader]); - } - - void linkProgram(Object program) { - js_util.callMethod(glContext, 'linkProgram', [program]); - if (!js_util - .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { - throw Exception(getProgramInfoLog(program)); - } - } - - void useProgram(GlProgram program) { - js_util.callMethod(glContext, 'useProgram', [program.program]); - } - - Object? createBuffer() => - js_util.callMethod(glContext, 'createBuffer', const []); - - void bindArrayBuffer(Object? buffer) { - js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); - } - - Object? createVertexArray() => - js_util.callMethod(glContext, 'createVertexArray', const []); - - void bindVertexArray(Object vertexObjectArray) { - js_util.callMethod(glContext, 'bindVertexArray', - [vertexObjectArray]); - } - - void unbindVertexArray() { - js_util.callMethod(glContext, 'bindVertexArray', - [null]); - } - - void bindElementArrayBuffer(Object? buffer) { - js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); - } - - Object? createTexture() => - js_util.callMethod(glContext, 'createTexture', const []); - - void generateMipmap(dynamic target) => - js_util.callMethod(glContext, 'generateMipmap', [target]); - - void bindTexture(dynamic target, Object? buffer) { - js_util.callMethod(glContext, 'bindTexture', [target, buffer]); - } - - void activeTexture(int textureUnit) { - js_util.callMethod(glContext, 'activeTexture', [textureUnit]); - } - - void texImage2D(dynamic target, int level, dynamic internalFormat, - dynamic format, dynamic dataType, - dynamic pixels, {int? width, int? height, int border = 0}) { - if (width == null) { - js_util.callMethod(glContext, 'texImage2D', [ - target, level, internalFormat, format, dataType, pixels]); - } else { - js_util.callMethod(glContext, 'texImage2D', [ - target, level, internalFormat, width, height, border, format, dataType, - pixels]); - } - } - - void texParameteri(dynamic target, dynamic parameterName, dynamic value) { - js_util.callMethod(glContext, 'texParameteri', [ - target, parameterName, value]); - } - - void deleteBuffer(Object buffer) { - js_util.callMethod(glContext, 'deleteBuffer', [buffer]); - } - - void bufferData(TypedData? data, dynamic type) { - js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); - } - - void bufferElementData(TypedData? data, dynamic type) { - js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); - } - - void enableVertexAttribArray(dynamic index) { - js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); - } - - /// Clear background. - void clear() { - js_util.callMethod(glContext, 'clear', [kColorBufferBit]); - } - - /// Destroys gl context. - void dispose() { - js_util.callMethod(_getExtension('WEBGL_lose_context'), 'loseContext', const []); - } - - void deleteProgram(Object program) { - js_util.callMethod(glContext, 'deleteProgram', [program]); - } - - void deleteShader(Object shader) { - js_util.callMethod(glContext, 'deleteShader', [shader]); - } - - dynamic _getExtension(String extensionName) => - js_util.callMethod(glContext, 'getExtension', [extensionName]); - - void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { - dynamic mode = _triangleTypeFromMode(vertexMode); - js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); - } - - void drawElements(dynamic type, int indexCount, dynamic indexType) { - js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); - } - - /// Sets affine transformation from normalized device coordinates - /// to window coordinates - void viewport(double x, double y, double width, double height) { - js_util.callMethod(glContext, 'viewport', [x, y, width, height]); - } - - dynamic _triangleTypeFromMode(ui.VertexMode mode) { - switch (mode) { - case ui.VertexMode.triangles: - return kTriangles; - case ui.VertexMode.triangleFan: - return kTriangleFan; - case ui.VertexMode.triangleStrip: - return kTriangleStrip; - } - } - - Object? _createShader(String shaderType) => js_util.callMethod( - glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); - - /// Error state of gl context. - dynamic get error => js_util.callMethod(glContext, 'getError', const []); - - /// Shader compiler error, if this returns [kFalse], to get details use - /// [getShaderInfoLog]. - dynamic get compileStatus => - _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); - - dynamic get kArrayBuffer => - _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); - - dynamic get kElementArrayBuffer => - _kElementArrayBuffer ??= js_util.getProperty(glContext, - 'ELEMENT_ARRAY_BUFFER'); - - dynamic get kLinkStatus => - _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); - - dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); - - dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); - - dynamic get kUnsignedByte => - _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); - - dynamic get kUnsignedShort => - _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); - - dynamic get kStaticDraw => - _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); - - dynamic get kTriangles => - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); - - dynamic get kTriangleFan => - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); - - dynamic get kTriangleStrip => - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); - - dynamic get kColorBufferBit => - _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); - - dynamic get kTexture2D => - _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); - - int get kTexture0 => - _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0') as int; - - dynamic get kTextureWrapS => - _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); - - dynamic get kTextureWrapT => - _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); - - dynamic get kRepeat => - _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); - - dynamic get kClampToEdge => - _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); - - dynamic get kMirroredRepeat => - _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); - - dynamic get kLinear => - _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); - - dynamic get kTextureMinFilter => - _kTextureMinFilter ??= js_util.getProperty(glContext, - 'TEXTURE_MIN_FILTER'); - - /// Returns reference to uniform in program. - Object getUniformLocation(Object program, String uniformName) { - Object? res = js_util - .callMethod(glContext, 'getUniformLocation', [program, uniformName]); - if (res == null) { - throw Exception('$uniformName not found'); - } else { - return res; - } - } - - /// Returns true if uniform exists. - bool containsUniform(Object program, String uniformName) { - Object? res = js_util - .callMethod(glContext, 'getUniformLocation', [program, uniformName]); - return res != null; - } - - /// Returns reference to uniform in program. - Object getAttributeLocation(Object program, String attribName) { - Object? res = js_util - .callMethod(glContext, 'getAttribLocation', [program, attribName]); - if (res == null) { - throw Exception('$attribName not found'); - } else { - return res; - } - } - - /// Sets float uniform value. - void setUniform1f(Object uniform, double value) { - return js_util - .callMethod(glContext, 'uniform1f', [uniform, value]); - } - - /// Sets vec2 uniform values. - void setUniform2f(Object uniform, double value1, double value2) { - return js_util - .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); - } - - /// Sets vec4 uniform values. - void setUniform4f(Object uniform, double value1, double value2, double value3, - double value4) { - return js_util.callMethod( - glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); - } - - /// Sets mat4 uniform values. - void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { - return js_util.callMethod( - glContext, 'uniformMatrix4fv', [uniform, transpose, value]); - } - - /// Shader compile error log. - dynamic getShaderInfoLog(Object glShader) { - return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); - } - - /// Errors that occurred during failed linking or validation of program - /// objects. Typically called after [linkProgram]. - String? getProgramInfoLog(Object glProgram) { - return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); - } - - int? get drawingBufferWidth => - js_util.getProperty(glContext, 'drawingBufferWidth'); - int? get drawingBufferHeight => - js_util.getProperty(glContext, 'drawingBufferWidth'); - - /// Reads gl contents as image data. - /// - /// Warning: data is read bottom up (flipped). - html.ImageData readImageData() { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; - if (browserEngine == BrowserEngine.webkit || - browserEngine == BrowserEngine.firefox) { - final Uint8List pixels = - Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); - return html.ImageData( - Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); - } else { - final Uint8ClampedList pixels = - Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); - js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); - return html.ImageData(pixels, bufferWidth, bufferHeight); - } - } - - /// Returns image data in a form that can be used to create Canvas - /// context patterns. - Object? readPatternData() { - // When using OffscreenCanvas and transferToImageBitmap is supported by - // browser create ImageBitmap otherwise use more expensive canvas - // allocation. - if (_canvas != null && - js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { - js_util.callMethod(_canvas!, 'getContext', ['webgl2']); - Object?imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', - []); - return imageBitmap; - } else { - html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); - final html.CanvasRenderingContext2D ctx = canvas.context2D; - drawImage(ctx, 0, 0); - return canvas; - } - } - - /// Returns image data in data url format. - String toImageUrl() { - html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); - final html.CanvasRenderingContext2D ctx = canvas.context2D; - drawImage(ctx, 0, 0); - final String dataUrl = canvas.toDataUrl(); - canvas.width = 0; - canvas.height = 0; - return dataUrl; - } -} - -/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. -class _OffScreenCanvas { - html.OffscreenCanvas? _canvas; - html.CanvasElement? _glCanvas; - int width; - int height; - - _OffScreenCanvas(this.width, this.height) { - if (_OffScreenCanvas.supported) { - _canvas = html.OffscreenCanvas(width, height); - } else { - _glCanvas = html.CanvasElement( - width: width, - height: height, - ); - _glCanvas!.className = 'gl-canvas'; - final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio; - final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio; - _glCanvas!.style - ..position = 'absolute' - ..width = '${cssWidth}px' - ..height = '${cssHeight}px'; - } - } - - void dispose() { - _canvas = null; - _glCanvas = null; - } - - /// Feature detects OffscreenCanvas. - static bool get supported => - js_util.hasProperty(html.window, 'OffscreenCanvas'); -} - -/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. -class _GlContextCache { - static int _maxPixelWidth = 0; - static int _maxPixelHeight = 0; - static GlContext? _cachedContext; - static _OffScreenCanvas? _offScreenCanvas; - - static void dispose() { - _maxPixelWidth = 0; - _maxPixelHeight = 0; - _cachedContext = null; - _offScreenCanvas?.dispose(); - } - - static GlContext? createGlContext(int widthInPixels, int heightInPixels) { - if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { - _cachedContext?.dispose(); - _cachedContext = null; - _offScreenCanvas = null; - _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); - _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); - } - _offScreenCanvas ??= _OffScreenCanvas(widthInPixels, heightInPixels); - if (_OffScreenCanvas.supported) { - _cachedContext ??= - GlContext.fromOffscreenCanvas(_offScreenCanvas!._canvas!); - } else { - _cachedContext ??= GlContext.fromCanvas(_offScreenCanvas!._glCanvas!, - webGLVersion == WebGLVersion.webgl1); - } - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; - } -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart index 33aebf19ad..de20261611 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -40,12 +40,9 @@ class GradientSweep extends EngineGradient { initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - _OffScreenCanvas offScreenCanvas = - _OffScreenCanvas(widthInPixels, heightInPixels); - GlContext gl = _OffScreenCanvas.supported - ? GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) - : GlContext.fromCanvas( - offScreenCanvas._glCanvas!, webGLVersion == WebGLVersion.webgl1); + OffScreenCanvas offScreenCanvas = + OffScreenCanvas(widthInPixels, heightInPixels); + GlContext gl = GlContext(offScreenCanvas); gl.setViewportSize(widthInPixels, heightInPixels); NormalizedGradient normalizedGradient = @@ -222,12 +219,9 @@ class GradientLinear extends EngineGradient { assert(widthInPixels > 0 && heightInPixels > 0); initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - _OffScreenCanvas offScreenCanvas = - _OffScreenCanvas(widthInPixels, heightInPixels); - GlContext gl = _OffScreenCanvas.supported - ? GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) - : GlContext.fromCanvas( - offScreenCanvas._glCanvas!, webGLVersion == WebGLVersion.webgl1); + OffScreenCanvas offScreenCanvas = + OffScreenCanvas(widthInPixels, heightInPixels); + GlContext gl = GlContext(offScreenCanvas); gl.setViewportSize(widthInPixels, heightInPixels); NormalizedGradient normalizedGradient = @@ -493,12 +487,9 @@ class GradientRadial extends EngineGradient { initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - _OffScreenCanvas offScreenCanvas = - _OffScreenCanvas(widthInPixels, heightInPixels); - GlContext gl = _OffScreenCanvas.supported - ? GlContext.fromOffscreenCanvas(offScreenCanvas._canvas!) - : GlContext.fromCanvas( - offScreenCanvas._glCanvas!, webGLVersion == WebGLVersion.webgl1); + OffScreenCanvas offScreenCanvas = + OffScreenCanvas(widthInPixels, heightInPixels); + GlContext gl = GlContext(offScreenCanvas); gl.setViewportSize(widthInPixels, heightInPixels); NormalizedGradient normalizedGradient = diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart new file mode 100644 index 0000000000..f53a510671 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart @@ -0,0 +1,488 @@ +// 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:html' as html; +import 'dart:js_util' as js_util; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:ui/ui.dart' as ui; + +import '../offscreen_canvas.dart'; +import '../../browser_detection.dart'; + +/// Compiled and cached gl program. +class GlProgram { + final Object program; + GlProgram(this.program); +} + +/// JS Interop helper for webgl apis. +class GlContext { + final Object glContext; + final bool isOffscreen; + dynamic _kCompileStatus; + dynamic _kArrayBuffer; + dynamic _kElementArrayBuffer; + dynamic _kStaticDraw; + dynamic _kFloat; + dynamic _kColorBufferBit; + dynamic _kTexture2D; + dynamic _kTextureWrapS; + dynamic _kTextureWrapT; + dynamic _kRepeat; + dynamic _kClampToEdge; + dynamic _kMirroredRepeat; + dynamic _kTriangles; + dynamic _kLinkStatus; + dynamic _kUnsignedByte; + dynamic _kUnsignedShort; + dynamic _kRGBA; + dynamic _kLinear; + dynamic _kTextureMinFilter; + int? _kTexture0; + + Object? _canvas; + int? _widthInPixels; + int? _heightInPixels; + static late Map _programCache; + + factory GlContext(OffScreenCanvas offScreenCanvas) { + return OffScreenCanvas.supported + ? GlContext._fromOffscreenCanvas(offScreenCanvas.offScreenCanvas!) + : GlContext._fromCanvasElement( + offScreenCanvas.canvasElement!, webGLVersion == WebGLVersion.webgl1); + } + + GlContext._fromOffscreenCanvas(html.OffscreenCanvas canvas) + : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, + isOffscreen = true { + _programCache = {}; + _canvas = canvas; + } + + GlContext._fromCanvasElement(html.CanvasElement canvas, bool useWebGl1) + : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', + {'premultipliedAlpha': false})!, + isOffscreen = false { + _programCache = {}; + _canvas = canvas; + } + + void setViewportSize(int width, int height) { + _widthInPixels = width; + _heightInPixels = height; + } + + /// Draws Gl context contents to canvas context. + void drawImage(html.CanvasRenderingContext2D context, + double left, double top) { + // Actual size of canvas may be larger than viewport size. Use + // source/destination to draw part of the image data. + js_util.callMethod(context, 'drawImage', + [_canvas, 0, 0, _widthInPixels, _heightInPixels, + left, top, _widthInPixels, _heightInPixels]); + } + + GlProgram cacheProgram( + String vertexShaderSource, String fragmentShaderSource) { + String cacheKey = '$vertexShaderSource||$fragmentShaderSource'; + GlProgram? cachedProgram = _programCache[cacheKey]; + if (cachedProgram == null) { + // Create and compile shaders. + Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); + Object fragmentShader = + compileShader('FRAGMENT_SHADER', fragmentShaderSource); + // Create a gl program and link shaders. + Object program = createProgram(); + attachShader(program, vertexShader); + attachShader(program, fragmentShader); + linkProgram(program); + cachedProgram = GlProgram(program); + _programCache[cacheKey] = cachedProgram; + } + return cachedProgram; + } + + Object compileShader(String shaderType, String source) { + Object? shader = _createShader(shaderType); + if (shader == null) { + throw Exception(error); + } + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); + bool shaderStatus = js_util + .callMethod(glContext, 'getShaderParameter', [shader, compileStatus]); + if (!shaderStatus) { + throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); + } + return shader; + } + + Object createProgram() => + js_util.callMethod(glContext, 'createProgram', const [])!; + + void attachShader(Object? program, Object shader) { + js_util.callMethod(glContext, 'attachShader', [program, shader]); + } + + void linkProgram(Object program) { + js_util.callMethod(glContext, 'linkProgram', [program]); + if (!js_util + .callMethod(glContext, 'getProgramParameter', [program, kLinkStatus])) { + throw Exception(getProgramInfoLog(program)); + } + } + + void useProgram(GlProgram program) { + js_util.callMethod(glContext, 'useProgram', [program.program]); + } + + Object? createBuffer() => + js_util.callMethod(glContext, 'createBuffer', const []); + + void bindArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); + } + + Object? createVertexArray() => + js_util.callMethod(glContext, 'createVertexArray', const []); + + void bindVertexArray(Object vertexObjectArray) { + js_util.callMethod(glContext, 'bindVertexArray', + [vertexObjectArray]); + } + + void unbindVertexArray() { + js_util.callMethod(glContext, 'bindVertexArray', + [null]); + } + + void bindElementArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); + } + + Object? createTexture() => + js_util.callMethod(glContext, 'createTexture', const []); + + void generateMipmap(dynamic target) => + js_util.callMethod(glContext, 'generateMipmap', [target]); + + void bindTexture(dynamic target, Object? buffer) { + js_util.callMethod(glContext, 'bindTexture', [target, buffer]); + } + + void activeTexture(int textureUnit) { + js_util.callMethod(glContext, 'activeTexture', [textureUnit]); + } + + void texImage2D(dynamic target, int level, dynamic internalFormat, + dynamic format, dynamic dataType, + dynamic pixels, {int? width, int? height, int border = 0}) { + if (width == null) { + js_util.callMethod(glContext, 'texImage2D', [ + target, level, internalFormat, format, dataType, pixels]); + } else { + js_util.callMethod(glContext, 'texImage2D', [ + target, level, internalFormat, width, height, border, format, dataType, + pixels]); + } + } + + void texParameteri(dynamic target, dynamic parameterName, dynamic value) { + js_util.callMethod(glContext, 'texParameteri', [ + target, parameterName, value]); + } + + void deleteBuffer(Object buffer) { + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); + } + + void bufferData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + } + + void bufferElementData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); + } + + void enableVertexAttribArray(dynamic index) { + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); + } + + /// Clear background. + void clear() { + js_util.callMethod(glContext, 'clear', [kColorBufferBit]); + } + + /// Destroys gl context. + void dispose() { + js_util.callMethod(_getExtension('WEBGL_lose_context'), 'loseContext', const []); + } + + void deleteProgram(Object program) { + js_util.callMethod(glContext, 'deleteProgram', [program]); + } + + void deleteShader(Object shader) { + js_util.callMethod(glContext, 'deleteShader', [shader]); + } + + dynamic _getExtension(String extensionName) => + js_util.callMethod(glContext, 'getExtension', [extensionName]); + + void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { + dynamic mode = _triangleTypeFromMode(vertexMode); + js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); + } + + void drawElements(dynamic type, int indexCount, dynamic indexType) { + js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); + } + + /// Sets affine transformation from normalized device coordinates + /// to window coordinates + void viewport(double x, double y, double width, double height) { + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); + } + + dynamic _triangleTypeFromMode(ui.VertexMode mode) { + switch (mode) { + case ui.VertexMode.triangles: + return kTriangles; + case ui.VertexMode.triangleFan: + return kTriangleFan; + case ui.VertexMode.triangleStrip: + return kTriangleStrip; + } + } + + Object? _createShader(String shaderType) => js_util.callMethod( + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); + + /// Error state of gl context. + dynamic get error => js_util.callMethod(glContext, 'getError', const []); + + /// Shader compiler error, if this returns [kFalse], to get details use + /// [getShaderInfoLog]. + dynamic get compileStatus => + _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); + + dynamic get kArrayBuffer => + _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); + + dynamic get kElementArrayBuffer => + _kElementArrayBuffer ??= js_util.getProperty(glContext, + 'ELEMENT_ARRAY_BUFFER'); + + dynamic get kLinkStatus => + _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); + + dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); + + dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); + + dynamic get kUnsignedByte => + _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); + + dynamic get kUnsignedShort => + _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); + + dynamic get kStaticDraw => + _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); + + dynamic get kTriangles => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); + + dynamic get kTriangleFan => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); + + dynamic get kTriangleStrip => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); + + dynamic get kColorBufferBit => + _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); + + dynamic get kTexture2D => + _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); + + int get kTexture0 => + _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0') as int; + + dynamic get kTextureWrapS => + _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); + + dynamic get kTextureWrapT => + _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); + + dynamic get kRepeat => + _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); + + dynamic get kClampToEdge => + _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); + + dynamic get kMirroredRepeat => + _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); + + dynamic get kLinear => + _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); + + dynamic get kTextureMinFilter => + _kTextureMinFilter ??= js_util.getProperty(glContext, + 'TEXTURE_MIN_FILTER'); + + /// Returns reference to uniform in program. + Object getUniformLocation(Object program, String uniformName) { + Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + if (res == null) { + throw Exception('$uniformName not found'); + } else { + return res; + } + } + + /// Returns true if uniform exists. + bool containsUniform(Object program, String uniformName) { + Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + return res != null; + } + + /// Returns reference to uniform in program. + Object getAttributeLocation(Object program, String attribName) { + Object? res = js_util + .callMethod(glContext, 'getAttribLocation', [program, attribName]); + if (res == null) { + throw Exception('$attribName not found'); + } else { + return res; + } + } + + /// Sets float uniform value. + void setUniform1f(Object uniform, double value) { + return js_util + .callMethod(glContext, 'uniform1f', [uniform, value]); + } + + /// Sets vec2 uniform values. + void setUniform2f(Object uniform, double value1, double value2) { + return js_util + .callMethod(glContext, 'uniform2f', [uniform, value1, value2]); + } + + /// Sets vec4 uniform values. + void setUniform4f(Object uniform, double value1, double value2, double value3, + double value4) { + return js_util.callMethod( + glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); + } + + /// Sets mat4 uniform values. + void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { + return js_util.callMethod( + glContext, 'uniformMatrix4fv', [uniform, transpose, value]); + } + + /// Shader compile error log. + dynamic getShaderInfoLog(Object glShader) { + return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); + } + + /// Errors that occurred during failed linking or validation of program + /// objects. Typically called after [linkProgram]. + String? getProgramInfoLog(Object glProgram) { + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); + } + + int? get drawingBufferWidth => + js_util.getProperty(glContext, 'drawingBufferWidth'); + int? get drawingBufferHeight => + js_util.getProperty(glContext, 'drawingBufferWidth'); + + /// Reads gl contents as image data. + /// + /// Warning: data is read bottom up (flipped). + html.ImageData readImageData() { + const int kBytesPerPixel = 4; + final int bufferWidth = _widthInPixels!; + final int bufferHeight = _heightInPixels!; + if (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.firefox) { + final Uint8List pixels = + Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); + js_util.callMethod(glContext, 'readPixels', + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + return html.ImageData( + Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); + } else { + final Uint8ClampedList pixels = + Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); + js_util.callMethod(glContext, 'readPixels', + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + return html.ImageData(pixels, bufferWidth, bufferHeight); + } + } + + /// Returns image data in a form that can be used to create Canvas + /// context patterns. + Object? readPatternData() { + // When using OffscreenCanvas and transferToImageBitmap is supported by + // browser create ImageBitmap otherwise use more expensive canvas + // allocation. + if (_canvas != null && + js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + js_util.callMethod(_canvas!, 'getContext', ['webgl2']); + Object?imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', + []); + return imageBitmap; + } else { + html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + return canvas; + } + } + + /// Returns image data in data url format. + String toImageUrl() { + html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + final String dataUrl = canvas.toDataUrl(); + canvas.width = 0; + canvas.height = 0; + return dataUrl; + } +} + +/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. +class GlContextCache { + static int _maxPixelWidth = 0; + static int _maxPixelHeight = 0; + static GlContext? _cachedContext; + static OffScreenCanvas? _offScreenCanvas; + + static void dispose() { + _maxPixelWidth = 0; + _maxPixelHeight = 0; + _cachedContext = null; + _offScreenCanvas?.dispose(); + } + + static GlContext? createGlContext(int widthInPixels, int heightInPixels) { + if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { + _cachedContext?.dispose(); + _cachedContext = null; + _offScreenCanvas = null; + _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); + _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); + } + _offScreenCanvas ??= OffScreenCanvas(widthInPixels, heightInPixels); + _cachedContext ??= GlContext(_offScreenCanvas!); + _cachedContext!.setViewportSize(widthInPixels, heightInPixels); + return _cachedContext; + } +}