From af7fb1d68b3365f0fd644f07786f03e9789d605e Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Wed, 27 Apr 2022 14:14:01 -0700 Subject: [PATCH] Reuse the same temporary canvas for creating shader images (flutter/engine#32864) --- .../lib/src/engine/html/shaders/shader.dart | 155 ++++++++++-------- .../lib/src/engine/safe_browser_api.dart | 31 +++- .../html/shaders/gradient_golden_test.dart | 33 ++++ 3 files changed, 149 insertions(+), 70 deletions(-) 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 9d54123b5e8..ddcbb092a7b 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 @@ -22,6 +22,27 @@ import 'vertex_shaders.dart'; const double kFltEpsilon = 1.19209290E-07; // == 1 / (2 ^ 23) const double kFltEpsilonSquared = 1.19209290E-07 * 1.19209290E-07; +class SharedCanvas { + OffScreenCanvas? _canvas; + bool _checkedOut = false; + GlContext checkOutContext(int width, int height) { + assert(!_checkedOut); + _checkedOut = true; + if(_canvas == null) { + _canvas = OffScreenCanvas(width, height); + } else { + _canvas!.resize(width, height); + } + return GlContext(_canvas!); + } + + void checkInContext() { + assert(_checkedOut); + _checkedOut = false; + } +} +SharedCanvas _sharedCanvas = SharedCanvas(); + abstract class EngineGradient implements ui.Gradient { /// Hidden constructor to prevent subclassing. EngineGradient._(); @@ -58,9 +79,7 @@ class GradientSweep extends EngineGradient { initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - final OffScreenCanvas offScreenCanvas = - OffScreenCanvas(widthInPixels, heightInPixels); - final GlContext gl = GlContext(offScreenCanvas); + final GlContext gl = _sharedCanvas.checkOutContext(widthInPixels, heightInPixels); gl.setViewportSize(widthInPixels, heightInPixels); final NormalizedGradient normalizedGradient = @@ -82,23 +101,27 @@ class GradientSweep extends EngineGradient { final Object gradientMatrix = gl.getUniformLocation(glProgram.program, 'm_gradient'); gl.setUniformMatrix4fv(gradientMatrix, false, matrix4 ?? Matrix4.identity().storage); - if (createDataUrl) { - return glRenderer!.drawRectToImageUrl( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels); - } else { - return glRenderer!.drawRect( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels)!; - } + final Object result = () { + if (createDataUrl) { + return glRenderer!.drawRectToImageUrl( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels); + } else { + return glRenderer!.drawRect( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels)!; + } + }(); + _sharedCanvas.checkInContext(); + return result; } @override @@ -222,9 +245,7 @@ class GradientLinear extends EngineGradient { assert(widthInPixels > 0 && heightInPixels > 0); initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - final OffScreenCanvas offScreenCanvas = - OffScreenCanvas(widthInPixels, heightInPixels); - final GlContext gl = GlContext(offScreenCanvas); + final GlContext gl = _sharedCanvas.checkOutContext(widthInPixels, heightInPixels); gl.setViewportSize(widthInPixels, heightInPixels); final NormalizedGradient normalizedGradient = @@ -314,27 +335,31 @@ class GradientLinear extends EngineGradient { final Object uRes = gl.getUniformLocation(glProgram.program, 'u_resolution'); gl.setUniform2f(uRes, widthInPixels.toDouble(), heightInPixels.toDouble()); - if (createDataUrl) { - return glRenderer!.drawRectToImageUrl( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, - shaderBounds.height) /* !! shaderBounds */, - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels, - ); - } else { - return glRenderer!.drawRect( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, - shaderBounds.height) /* !! shaderBounds */, - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels, - )!; - } + final Object result = () { + if (createDataUrl) { + return glRenderer!.drawRectToImageUrl( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, + shaderBounds.height) /* !! shaderBounds */, + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels, + ); + } else { + return glRenderer!.drawRect( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, + shaderBounds.height) /* !! shaderBounds */, + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels, + )!; + } + }(); + _sharedCanvas.checkInContext(); + return result; } /// Creates a linear gradient with tiling repeat or mirror. @@ -490,9 +515,7 @@ class GradientRadial extends EngineGradient { initWebGl(); // Render gradient into a bitmap and create a canvas pattern. - final OffScreenCanvas offScreenCanvas = - OffScreenCanvas(widthInPixels, heightInPixels); - final GlContext gl = GlContext(offScreenCanvas); + final GlContext gl = _sharedCanvas.checkOutContext(widthInPixels, heightInPixels); gl.setViewportSize(widthInPixels, heightInPixels); final NormalizedGradient normalizedGradient = @@ -519,23 +542,27 @@ class GradientRadial extends EngineGradient { gl.setUniformMatrix4fv(gradientMatrix, false, matrix4 == null ? Matrix4.identity().storage : matrix4!); - if (createDataUrl) { - return glRenderer!.drawRectToImageUrl( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels); - } else { - return glRenderer!.drawRect( - ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), - gl, - glProgram, - normalizedGradient, - widthInPixels, - heightInPixels)!; - } + final Object result = () { + if (createDataUrl) { + return glRenderer!.drawRectToImageUrl( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels); + } else { + return glRenderer!.drawRect( + ui.Rect.fromLTWH(0, 0, shaderBounds.width, shaderBounds.height), + gl, + glProgram, + normalizedGradient, + widthInPixels, + heightInPixels)!; + } + }(); + _sharedCanvas.checkInContext(); + return result; } /// Creates a radial gradient with tiling repeat or mirror. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart index 2b1c4cd64c5..e686280b3da 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -966,12 +966,31 @@ class OffScreenCanvas { 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'; + _updateCanvasCssSize(canvasElement!); + } + } + + void _updateCanvasCssSize(html.CanvasElement element) { + final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio; + final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio; + element.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; + } + + void resize(int requestedWidth, int requestedHeight) { + if(requestedWidth != width && requestedHeight != height) { + width = requestedWidth; + height = requestedHeight; + if(offScreenCanvas != null) { + offScreenCanvas!.width = requestedWidth; + offScreenCanvas!.height = requestedHeight; + } else if (canvasElement != null) { + canvasElement!.width = requestedWidth; + canvasElement!.height = requestedHeight; + _updateCanvasCssSize(canvasElement!); + } } } diff --git a/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart b/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart index 6c667e8c991..1cbcdfe81b6 100644 --- a/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart +++ b/engine/src/flutter/lib/web_ui/test/html/shaders/gradient_golden_test.dart @@ -5,6 +5,7 @@ import 'dart:html' as html; import 'dart:math' as math; import 'dart:typed_data'; +import 'dart:web_gl'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; @@ -390,6 +391,38 @@ Future testMain() async { canvas.apply(engineCanvas, screenRect); }); + test('Creating lots of gradients doesn\'t create too many webgl contexts', + () async { + final html.CanvasElement sideCanvas = + html.CanvasElement(width: 5, height: 5); + final RenderingContext? context = + sideCanvas.getContext('webgl') as RenderingContext?; + expect(context, isNotNull); + + final EngineCanvas engineCanvas = + BitmapCanvas(const Rect.fromLTRB(0, 0, 100, 100), RenderStrategy()); + for (double x = 0; x < 100; x += 10) { + for (double y = 0; y < 100; y += 10) { + const List colors = [ + Color(0xFFFF0000), + Color(0xFF0000FF), + ]; + + final GradientLinear linearGradient = GradientLinear( + const Offset(0, 0), + const Offset(10, 10), + colors, + null, + TileMode.clamp, + Matrix4.identity().storage); + engineCanvas.drawRect(Rect.fromLTWH(x, y, 10, 10), + SurfacePaintData()..shader = linearGradient); + } + } + + expect(context!.isContextLost(), isFalse); + }, skip: isFirefox); + test('Paints clamped, rotated and shifted linear gradient', () async { final RecordingCanvas canvas = RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));