From 23e3f7d039487e567ed35348f10a20c33d42e3ca Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 11 Jan 2022 11:51:13 -0800 Subject: [PATCH] [web] do not allocate canvases just for text --- lib/web_ui/lib/src/engine/canvas_pool.dart | 33 +++++++++----- .../lib/src/engine/html/bitmap_canvas.dart | 44 +++++++++++-------- .../lib/src/engine/text/canvas_paragraph.dart | 23 +++++----- .../test/html/bitmap_canvas_golden_test.dart | 22 ++++++++++ .../compositing/compositing_golden_test.dart | 4 +- 5 files changed, 84 insertions(+), 42 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index 6528fae7eb346..7bf1b3890349b 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -76,13 +76,6 @@ class CanvasPool extends _SaveStackTracking { translate(transform.dx, transform.dy); } - /// Returns true if no canvas has been allocated yet. - bool get isEmpty => _canvas == null; - - /// Returns true if a canvas has been allocated for use. - bool get isNotEmpty => _canvas != null; - - /// Returns [CanvasRenderingContext2D] api to draw into this canvas. html.CanvasRenderingContext2D get context { html.CanvasRenderingContext2D? ctx = _context; @@ -106,12 +99,28 @@ class CanvasPool extends _SaveStackTracking { return _contextHandle!; } - /// Prevents active canvas to be used for rendering and prepares a new - /// canvas allocation on next drawing request that will require one. + /// Returns true if a canvas is currently available for drawing. + /// + /// Calling [contextHandle] or, transitively, any of the `draw*` methods while + /// this returns true will reuse the existing canvas. Otherwise, a new canvas + /// will be allocated. + /// + /// Previously allocated and closed canvases (see [closeCanvas]) are not + /// considered by this getter. + bool get hasCanvas => _canvas != null; + + /// Stops the currently available canvas from receiving any further drawing + /// commands. + /// + /// After calling this method, a subsequent call to [contextHandle] or, + /// transitively, any of the `draw*` methods will cause a new canvas to be + /// allocated. /// - /// Saves current canvas so we can dispose - /// and replay the clip/transform stack on top of new canvas. - void closeCurrentCanvas() { + /// The closed canvas becomes an "active" canvas, that is a canvas that's used + /// to render picture content in the current frame. Active canvases may be + /// reused in other pictures if their contents are no longer needed for this + /// picture. + void closeCanvas() { assert(_rootElement != null); // Place clean copy of current canvas with context stack restored and paint // reset into pool. diff --git a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart index 9b7bb4a011282..8b9677d94fcb6 100644 --- a/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart @@ -370,7 +370,7 @@ class BitmapCanvas extends EngineCanvas { _renderStrategy.isInsideSvgFilterTree || (_preserveImageData == false && _contains3dTransform) || (_childOverdraw && - _canvasPool.isEmpty && + !_canvasPool.hasCanvas && paint.maskFilter == null && paint.shader == null && paint.style != ui.PaintingStyle.stroke); @@ -384,7 +384,7 @@ class BitmapCanvas extends EngineCanvas { ((_childOverdraw || _renderStrategy.hasImageElements || _renderStrategy.hasParagraphs) && - _canvasPool.isEmpty && + !_canvasPool.hasCanvas && paint.maskFilter == null && paint.shader == null); @@ -469,7 +469,7 @@ class BitmapCanvas extends EngineCanvas { element.style.mixBlendMode = blendModeToCssMixBlendMode(blendMode) ?? ''; } // Switch to preferring DOM from now on, and close the current canvas. - _closeCurrentCanvas(); + _closeCanvas(); } @override @@ -626,7 +626,7 @@ class BitmapCanvas extends EngineCanvas { _applyTargetSize( imageElement, image.width.toDouble(), image.height.toDouble()); } - _closeCurrentCanvas(); + _closeCanvas(); } html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) { @@ -770,7 +770,7 @@ class BitmapCanvas extends EngineCanvas { restore(); } } - _closeCurrentCanvas(); + _closeCanvas(); } void _applyTargetSize( @@ -882,8 +882,8 @@ class BitmapCanvas extends EngineCanvas { // |--- // Any drawing operations after these tags should allocate a new canvas, // instead of drawing into earlier canvas. - void _closeCurrentCanvas() { - _canvasPool.closeCurrentCanvas(); + void _closeCanvas() { + _canvasPool.closeCanvas(); _childOverdraw = true; _cachedLastCssFont = null; } @@ -939,16 +939,24 @@ class BitmapCanvas extends EngineCanvas { void drawParagraph(CanvasParagraph paragraph, ui.Offset offset) { assert(paragraph.isLaidOut); - /// - paragraph.drawOnCanvas checks that the text styling doesn't include - /// features that prevent text from being rendered correctly using canvas. - /// - _childOverdraw check prevents sandwitching multiple canvas elements - /// when we have alternating paragraphs and other drawing commands that are - /// suitable for canvas. - /// - To make sure an svg filter is applied correctly to paragraph we - /// check isInsideSvgFilterTree to make sure dom node doesn't have any - /// parents that apply one. - if (paragraph.drawOnCanvas && _childOverdraw == false && - !_renderStrategy.isInsideSvgFilterTree) { + // Normally, text is composited as a plain HTML

tag. However, if a + // bitmap canvas was used for a preceding drawing command, then it's more + // efficient to continue compositing into the existing canvas, if possible. + // Whether it's possible to composite a paragraph into a 2D canvas depends + // on the following: + final bool canCompositeIntoBitmapCanvas = + // Cannot composite if the paragraph cannot be drawn into bitmap canvas + // in the first place. + paragraph.canDrawOnCanvas && + // Cannot composite if there's no bitmap canvas to composite into. + // Creating a new bitmap canvas just to draw text doesn't make sense. + _canvasPool.hasCanvas && + !_childOverdraw && + // Bitmap canvas introduces correctness issues in the presence of SVG + // filters, so prefer plain HTML in this case. + !_renderStrategy.isInsideSvgFilterTree; + + if (canCompositeIntoBitmapCanvas) { paragraph.paint(this, offset); return; } @@ -977,7 +985,7 @@ class BitmapCanvas extends EngineCanvas { paragraphElement.style ..left = '0px' ..top = '0px'; - _closeCurrentCanvas(); + _closeCanvas(); } /// Draws vertices on a gl context. diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart index 64c7bf30558fb..77f8f4ca00354 100644 --- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart @@ -31,7 +31,7 @@ class CanvasParagraph implements ui.Paragraph { required this.paragraphStyle, required this.plainText, required this.placeholderCount, - required this.drawOnCanvas, + required this.canDrawOnCanvas, }); /// The flat list of spans that make up this paragraph. @@ -47,7 +47,10 @@ class CanvasParagraph implements ui.Paragraph { final int placeholderCount; /// Whether this paragraph can be drawn on a bitmap canvas. - final bool drawOnCanvas; + /// + /// Some text features cannot be rendered into a 2D canvas and must use HTML, + /// such as font features and text decorations. + final bool canDrawOnCanvas; @override double get width => _layoutService.width; @@ -623,7 +626,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder { } } - bool _drawOnCanvas = true; + bool _canDrawOnCanvas = true; @override void addText(String text) { @@ -632,24 +635,24 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder { _plainTextBuffer.write(text); final int end = _plainTextBuffer.length; - if (_drawOnCanvas) { + if (_canDrawOnCanvas) { final ui.TextDecoration? decoration = style.decoration; if (decoration != null && decoration != ui.TextDecoration.none) { - _drawOnCanvas = false; + _canDrawOnCanvas = false; } } - if (_drawOnCanvas) { + if (_canDrawOnCanvas) { final List? fontFeatures = style.fontFeatures; if (fontFeatures != null && fontFeatures.isNotEmpty) { - _drawOnCanvas = false; + _canDrawOnCanvas = false; } } - if (_drawOnCanvas) { + if (_canDrawOnCanvas) { final List? fontVariations = style.fontVariations; if (fontVariations != null && fontVariations.isNotEmpty) { - _drawOnCanvas = false; + _canDrawOnCanvas = false; } } @@ -663,7 +666,7 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder { paragraphStyle: _paragraphStyle, plainText: _plainTextBuffer.toString(), placeholderCount: _placeholderCount, - drawOnCanvas: _drawOnCanvas, + canDrawOnCanvas: _canDrawOnCanvas, ); } } diff --git a/lib/web_ui/test/html/bitmap_canvas_golden_test.dart b/lib/web_ui/test/html/bitmap_canvas_golden_test.dart index 2d40b4b62fc3a..c6da523ff411e 100644 --- a/lib/web_ui/test/html/bitmap_canvas_golden_test.dart +++ b/lib/web_ui/test/html/bitmap_canvas_golden_test.dart @@ -270,4 +270,26 @@ Future testMain() async { pixelComparison: PixelComparison.precise, ); }); + + // Regression test for https://github.com/flutter/flutter/issues/96498. When + // a picture is made of just text that can be rendered using plain HTML, + // BitmapCanvas should not create any elements as they are expensive. + test('does not allocate bitmap canvas just for text', () async { + canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50), RenderStrategy()); + + final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto')); + builder.addText('Hello'); + final CanvasParagraph paragraph = builder.build() as CanvasParagraph; + paragraph.layout(const ParagraphConstraints(width: 1000)); + + canvas.drawParagraph(paragraph, const Offset(8.5, 8.5)); + expect( + canvas.rootElement.querySelectorAll('canvas'), + isEmpty, + ); + expect( + canvas.rootElement.querySelectorAll('flt-paragraph').single.innerText, + 'Hello', + ); + }); } diff --git a/lib/web_ui/test/html/compositing/compositing_golden_test.dart b/lib/web_ui/test/html/compositing/compositing_golden_test.dart index b6e3601696cae..e4b0a6b527e76 100644 --- a/lib/web_ui/test/html/compositing/compositing_golden_test.dart +++ b/lib/web_ui/test/html/compositing/compositing_golden_test.dart @@ -847,7 +847,7 @@ void _testCullRectComputation() { final RecordingCanvas canvas = recorder.beginRecording(outerClip); canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5)); final ui.Picture picture = recorder.endRecording(); - expect(paragraph.drawOnCanvas, isFalse); + expect(paragraph.canDrawOnCanvas, isFalse); builder.addPicture( ui.Offset.zero, @@ -861,7 +861,7 @@ void _testCullRectComputation() { final RecordingCanvas canvas = recorder.beginRecording(innerClip); canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top)); final ui.Picture picture = recorder.endRecording(); - expect(paragraph.drawOnCanvas, isFalse); + expect(paragraph.canDrawOnCanvas, isFalse); builder.addPicture( ui.Offset.zero,