• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5part of engine;
6
7// TODO(yjbanov): this is currently very naive. We probably want to cache
8//                fewer large canvases than small canvases. We could also
9//                improve cache hit count if we did not require exact canvas
10//                size match, but instead could choose a canvas that's big
11//                enough. The optimal heuristic will need to be figured out.
12//                For example, we probably don't want to pick a full-screen
13//                canvas to draw a 10x10 picture. Let's revisit this after
14//                Harry's layer merging refactor.
15/// The maximum number canvases cached.
16const int _kCanvasCacheSize = 30;
17
18/// Canvases available for reuse, capped at [_kCanvasCacheSize].
19final List<BitmapCanvas> _recycledCanvases = <BitmapCanvas>[];
20
21/// A request to repaint a canvas.
22///
23/// Paint requests are prioritized such that the larger pictures go first. This
24/// makes canvas allocation more efficient by letting large pictures claim
25/// larger recycled canvases. Otherwise, small pictures would claim the large
26/// canvases forcing us to allocate new large canvases.
27class _PaintRequest {
28  _PaintRequest({
29    this.canvasSize,
30    this.paintCallback,
31  })  : assert(canvasSize != null),
32        assert(paintCallback != null);
33
34  final ui.Size canvasSize;
35  final ui.VoidCallback paintCallback;
36}
37
38/// Repaint requests produced by [PersistedPicture]s that actually paint on the
39/// canvas. Painting is delayed until the layer tree is updated to maximize
40/// the number of reusable canvases.
41List<_PaintRequest> _paintQueue = <_PaintRequest>[];
42
43void _recycleCanvas(EngineCanvas canvas) {
44  if (canvas is BitmapCanvas && canvas.isReusable()) {
45    _recycledCanvases.add(canvas);
46    if (_recycledCanvases.length > _kCanvasCacheSize) {
47      final BitmapCanvas removedCanvas = _recycledCanvases.removeAt(0);
48      removedCanvas.dispose();
49      if (_debugShowCanvasReuseStats) {
50        DebugCanvasReuseOverlay.instance.disposedCount++;
51      }
52    }
53    if (_debugShowCanvasReuseStats) {
54      DebugCanvasReuseOverlay.instance.inRecycleCount =
55          _recycledCanvases.length;
56    }
57  }
58}
59
60/// Signature of a function that instantiates a [PersistedPicture].
61typedef PersistedPictureFactory = PersistedPicture Function(
62  double dx,
63  double dy,
64  ui.Picture picture,
65  int hints,
66);
67
68/// Function used by the [SceneBuilder] to instantiate a picture layer.
69PersistedPictureFactory persistedPictureFactory = standardPictureFactory;
70
71/// Instantiates an implementation of a picture layer that uses DOM, CSS, and
72/// 2D canvas for painting.
73PersistedStandardPicture standardPictureFactory(
74    double dx, double dy, ui.Picture picture, int hints) {
75  return PersistedStandardPicture(dx, dy, picture, hints);
76}
77
78/// Instantiates an implementation of a picture layer that uses CSS Paint API
79/// (part of Houdini) for painting.
80PersistedHoudiniPicture houdiniPictureFactory(
81    double dx, double dy, ui.Picture picture, int hints) {
82  return PersistedHoudiniPicture(dx, dy, picture, hints);
83}
84
85class PersistedHoudiniPicture extends PersistedPicture {
86  PersistedHoudiniPicture(double dx, double dy, ui.Picture picture, int hints)
87      : super(dx, dy, picture, hints) {
88    if (!_cssPainterRegistered) {
89      _registerCssPainter();
90    }
91  }
92
93  static bool _cssPainterRegistered = false;
94
95  @override
96  double matchForUpdate(PersistedPicture existingSurface) {
97    // Houdini is display list-based so all pictures are cheap to repaint.
98    // However, if the picture hasn't changed at all then it's completely
99    // free.
100    return existingSurface.picture == picture ? 0.0 : 1.0;
101  }
102
103  static void _registerCssPainter() {
104    _cssPainterRegistered = true;
105    final dynamic css = js_util.getProperty(html.window, 'CSS');
106    final dynamic paintWorklet = js_util.getProperty(css, 'paintWorklet');
107    if (paintWorklet == null) {
108      html.window.console.warn(
109          'WARNING: CSS.paintWorklet not available. Paint worklets are only '
110          'supported on sites served from https:// or http://localhost.');
111      return;
112    }
113    js_util.callMethod(
114      paintWorklet,
115      'addModule',
116      <dynamic>[
117        '/packages/flutter_web/assets/houdini_painter.js',
118      ],
119    );
120  }
121
122  /// Houdini does not paint to bitmap.
123  @override
124  int get bitmapPixelCount => 0;
125
126  @override
127  void applyPaint(EngineCanvas oldCanvas) {
128    _recycleCanvas(oldCanvas);
129    final HoudiniCanvas canvas = HoudiniCanvas(_optimalLocalCullRect);
130    _canvas = canvas;
131    domRenderer.clearDom(rootElement);
132    rootElement.append(_canvas.rootElement);
133    picture.recordingCanvas.apply(_canvas);
134    canvas.commit();
135  }
136}
137
138class PersistedStandardPicture extends PersistedPicture {
139  PersistedStandardPicture(double dx, double dy, ui.Picture picture, int hints)
140      : super(dx, dy, picture, hints);
141
142  @override
143  double matchForUpdate(PersistedStandardPicture existingSurface) {
144    if (existingSurface.picture == picture) {
145      // Picture is the same, return perfect score.
146      return 0.0;
147    }
148
149    if (!existingSurface.picture.recordingCanvas.didDraw) {
150      // The previous surface didn't draw anything and therefore has no
151      // resources to reuse.
152      return 1.0;
153    }
154
155    final bool didRequireBitmap =
156        existingSurface.picture.recordingCanvas.hasArbitraryPaint;
157    final bool requiresBitmap = picture.recordingCanvas.hasArbitraryPaint;
158    if (didRequireBitmap != requiresBitmap) {
159      // Switching canvas types is always expensive.
160      return 1.0;
161    } else if (!requiresBitmap) {
162      // Currently DomCanvas is always expensive to repaint, as we always throw
163      // out all the DOM we rendered before. This may change in the future, at
164      // which point we may return other values here.
165      return 1.0;
166    } else {
167      final BitmapCanvas oldCanvas = existingSurface._canvas;
168      if (!_doesCanvasFitBounds(oldCanvas, _exactLocalCullRect)) {
169        // The canvas needs to be resized before painting.
170        return 1.0;
171      } else {
172        final double newPixelCount =
173            _exactLocalCullRect.size.width * _exactLocalCullRect.size.height;
174        final double oldPixelCount =
175            oldCanvas.size.width * oldCanvas.size.height;
176
177        if (oldPixelCount == 0) {
178          return 1.0;
179        }
180
181        final double pixelCountRatio = newPixelCount / oldPixelCount;
182        assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0,
183            'Invalid pixel count ratio $pixelCountRatio');
184        return 1.0 - pixelCountRatio;
185      }
186    }
187  }
188
189  @override
190  int get bitmapPixelCount {
191    if (_canvas is! BitmapCanvas) {
192      return 0;
193    }
194
195    final BitmapCanvas bitmapCanvas = _canvas;
196    return bitmapCanvas.bitmapPixelCount;
197  }
198
199  FrameReference<bool> _didApplyPaint = FrameReference<bool>(false);
200
201  @override
202  void applyPaint(EngineCanvas oldCanvas) {
203    if (picture.recordingCanvas.hasArbitraryPaint) {
204      _applyBitmapPaint(oldCanvas);
205    } else {
206      _applyDomPaint(oldCanvas);
207    }
208    _didApplyPaint.value = true;
209  }
210
211  void _applyDomPaint(EngineCanvas oldCanvas) {
212    _recycleCanvas(oldCanvas);
213    _canvas = DomCanvas();
214    domRenderer.clearDom(rootElement);
215    rootElement.append(_canvas.rootElement);
216    picture.recordingCanvas.apply(_canvas);
217  }
218
219  static bool _doesCanvasFitBounds(BitmapCanvas canvas, ui.Rect newBounds) {
220    assert(canvas != null);
221    assert(newBounds != null);
222    final ui.Rect canvasBounds = canvas.bounds;
223    assert(canvasBounds != null);
224    return canvasBounds.width >= newBounds.width &&
225        canvasBounds.height >= newBounds.height;
226  }
227
228  void _applyBitmapPaint(EngineCanvas oldCanvas) {
229    if (oldCanvas is BitmapCanvas &&
230        _doesCanvasFitBounds(oldCanvas, _optimalLocalCullRect) &&
231        oldCanvas.isReusable()) {
232      if (_debugShowCanvasReuseStats) {
233        DebugCanvasReuseOverlay.instance.keptCount++;
234      }
235      oldCanvas.bounds = _optimalLocalCullRect;
236      _canvas = oldCanvas;
237      _canvas.clear();
238      picture.recordingCanvas.apply(_canvas);
239    } else {
240      // We can't use the old canvas because the size has changed, so we put
241      // it in a cache for later reuse.
242      _recycleCanvas(oldCanvas);
243      // We cannot paint immediately because not all canvases that we may be
244      // able to reuse have been released yet. So instead we enqueue this
245      // picture to be painted after the update cycle is done syncing the layer
246      // tree then reuse canvases that were freed up.
247      _paintQueue.add(_PaintRequest(
248        canvasSize: _optimalLocalCullRect.size,
249        paintCallback: () {
250          _canvas = _findOrCreateCanvas(_optimalLocalCullRect);
251          if (_debugExplainSurfaceStats) {
252            final BitmapCanvas bitmapCanvas = _canvas;
253            _surfaceStatsFor(this).paintPixelCount +=
254                bitmapCanvas.bitmapPixelCount;
255          }
256          domRenderer.clearDom(rootElement);
257          rootElement.append(_canvas.rootElement);
258          _canvas.clear();
259          picture.recordingCanvas.apply(_canvas);
260        },
261      ));
262    }
263  }
264
265  /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new
266  /// one if unable to reuse.
267  ///
268  /// The best recycled canvas is one that:
269  ///
270  /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise
271  ///   we risk clipping the picture.
272  /// - Is the smallest among all possible reusable canvases. This makes canvas
273  ///   reuse more efficient.
274  /// - Contains no more than twice the number of requested pixels. This makes
275  ///   sure we do not use too much memory for small canvases.
276  BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) {
277    final ui.Size canvasSize = bounds.size;
278    BitmapCanvas bestRecycledCanvas;
279    double lastPixelCount = double.infinity;
280
281    for (int i = 0; i < _recycledCanvases.length; i++) {
282      final BitmapCanvas candidate = _recycledCanvases[i];
283      if (!candidate.isReusable()) {
284        continue;
285      }
286
287      final ui.Size candidateSize = candidate.size;
288      final double candidatePixelCount =
289          candidateSize.width * candidateSize.height;
290
291      final bool fits = _doesCanvasFitBounds(candidate, bounds);
292      final bool isSmaller = candidatePixelCount < lastPixelCount;
293      if (fits && isSmaller) {
294        bestRecycledCanvas = candidate;
295        lastPixelCount = candidatePixelCount;
296        final bool fitsExactly = candidateSize.width == canvasSize.width &&
297            candidateSize.height == canvasSize.height;
298        if (fitsExactly) {
299          // No need to keep looking any more.
300          break;
301        }
302      }
303    }
304
305    if (bestRecycledCanvas != null) {
306      if (_debugExplainSurfaceStats) {
307        _surfaceStatsFor(this).reuseCanvasCount++;
308      }
309      _recycledCanvases.remove(bestRecycledCanvas);
310      if (_debugShowCanvasReuseStats) {
311        DebugCanvasReuseOverlay.instance.inRecycleCount =
312            _recycledCanvases.length;
313      }
314      if (_debugShowCanvasReuseStats) {
315        DebugCanvasReuseOverlay.instance.reusedCount++;
316      }
317      bestRecycledCanvas.bounds = bounds;
318      return bestRecycledCanvas;
319    }
320
321    if (_debugShowCanvasReuseStats) {
322      DebugCanvasReuseOverlay.instance.createdCount++;
323    }
324    final BitmapCanvas canvas = BitmapCanvas(bounds);
325    if (_debugExplainSurfaceStats) {
326      _surfaceStatsFor(this)
327        ..allocateBitmapCanvasCount += 1
328        ..allocatedBitmapSizeInPixels =
329            canvas.widthInBitmapPixels * canvas.heightInBitmapPixels;
330    }
331    return canvas;
332  }
333}
334
335/// A surface that uses a combination of `<canvas>`, `<div>` and `<p>` elements
336/// to draw shapes and text.
337abstract class PersistedPicture extends PersistedLeafSurface {
338  PersistedPicture(this.dx, this.dy, this.picture, this.hints)
339      : localPaintBounds = picture.recordingCanvas.computePaintBounds();
340
341  EngineCanvas _canvas;
342
343  final double dx;
344  final double dy;
345  final ui.Picture picture;
346  final ui.Rect localPaintBounds;
347  final int hints;
348
349  @override
350  html.Element createElement() {
351    return defaultCreateElement('flt-picture');
352  }
353
354  @override
355  void recomputeTransformAndClip() {
356    _transform = parent._transform;
357    if (dx != 0.0 || dy != 0.0) {
358      _transform = _transform.clone();
359      _transform.translate(dx, dy);
360    }
361    _globalClip = parent._globalClip;
362    _computeExactCullRects();
363  }
364
365  /// The rectangle that contains all visible pixels drawn by [picture] inside
366  /// the current layer hierarchy in local coordinates.
367  ///
368  /// This value is a conservative estimate, i.e. it must be big enough to
369  /// contain everything that's visible, but it may be bigger than necessary.
370  /// Therefore it should not be used for clipping. It is meant to be used for
371  /// optimizing canvas allocation.
372  ui.Rect get optimalLocalCullRect => _optimalLocalCullRect;
373  ui.Rect _optimalLocalCullRect;
374
375  /// Same as [optimalLocalCullRect] but in screen coordinate system.
376  ui.Rect get debugExactGlobalCullRect => _exactGlobalCullRect;
377  ui.Rect _exactGlobalCullRect;
378
379  ui.Rect _exactLocalCullRect;
380
381  /// Computes the canvas paint bounds based on the estimated paint bounds and
382  /// the scaling produced by transformations.
383  ///
384  /// Return `true` if the local cull rect changed, indicating that a repaint
385  /// may be required. Returns `false` otherwise. Global cull rect changes do
386  /// not necessarily incur repaints. For example, if the layer sub-tree was
387  /// translated from one frame to another we may not need to repaint, just
388  /// translate the canvas.
389  void _computeExactCullRects() {
390    assert(transform != null);
391    assert(localPaintBounds != null);
392    final ui.Rect globalPaintBounds = localClipRectToGlobalClip(
393        localClip: localPaintBounds, transform: transform);
394
395    // The exact cull rect required in screen coordinates.
396    ui.Rect tightGlobalCullRect = globalPaintBounds.intersect(_globalClip);
397
398    // The exact cull rect required in local coordinates.
399    ui.Rect tightLocalCullRect;
400    if (tightGlobalCullRect.width <= 0 || tightGlobalCullRect.height <= 0) {
401      tightGlobalCullRect = ui.Rect.zero;
402      tightLocalCullRect = ui.Rect.zero;
403    } else {
404      final Matrix4 invertedTransform =
405          Matrix4.fromFloat64List(Float64List(16));
406
407      // TODO(yjbanov): When we move to our own vector math library, rewrite
408      //                this to check for the case of simple transform before
409      //                inverting. Inversion of simple transforms can be made
410      //                much cheaper.
411      final double det = invertedTransform.copyInverse(transform);
412      if (det == 0) {
413        // Determinant is zero, which means the transform is not invertible.
414        tightGlobalCullRect = ui.Rect.zero;
415        tightLocalCullRect = ui.Rect.zero;
416      } else {
417        tightLocalCullRect = localClipRectToGlobalClip(
418            localClip: tightGlobalCullRect, transform: invertedTransform);
419      }
420    }
421
422    assert(tightLocalCullRect != null);
423    _exactLocalCullRect = tightLocalCullRect;
424    _exactGlobalCullRect = tightGlobalCullRect;
425  }
426
427  bool _computeOptimalCullRect(PersistedPicture oldSurface) {
428    assert(_exactLocalCullRect != null);
429
430    if (oldSurface == null || !oldSurface.picture.recordingCanvas.didDraw) {
431      // First useful paint.
432      _optimalLocalCullRect = _exactLocalCullRect;
433      return true;
434    }
435
436    assert(oldSurface._optimalLocalCullRect != null);
437
438    final bool surfaceBeingRetained = identical(oldSurface, this);
439    final ui.Rect oldOptimalLocalCullRect = surfaceBeingRetained
440        ? _optimalLocalCullRect
441        : oldSurface._optimalLocalCullRect;
442
443    if (_exactLocalCullRect == ui.Rect.zero) {
444      // The clip collapsed into a zero-sized rectangle. If it was already zero,
445      // no need to signal cull rect change.
446      _optimalLocalCullRect = ui.Rect.zero;
447      return oldOptimalLocalCullRect != ui.Rect.zero;
448    }
449
450    if (rectContainsOther(oldOptimalLocalCullRect, _exactLocalCullRect)) {
451      // The cull rect we computed in the past contains the newly computed cull
452      // rect. This can happen, for example, when the picture is being shrunk by
453      // a clip when it is scrolled out of the screen. In this case we do not
454      // repaint the picture. We just let it be shrunk by the outer clip.
455      _optimalLocalCullRect = oldOptimalLocalCullRect;
456      return false;
457    }
458
459    // The new cull rect contains area not covered by a previous rect. Perhaps
460    // the clip is growing, moving around the picture, or both. In this case
461    // a part of the picture may not been painted. We will need to
462    // request a new canvas and paint the picture on it. However, this is also
463    // a strong signal that the clip will continue growing as typically
464    // Flutter uses animated transitions. So instead of allocating the canvas
465    // the size of the currently visible area, we try to allocate a canvas of
466    // a bigger size. This will prevent any further repaints as future frames
467    // will hit the above case where the new cull rect is fully contained
468    // within the cull rect we compute now.
469
470    // If any of the borders moved.
471    // TODO(yjbanov): consider switching to Mouad's snap-to-10px strategy. It
472    //                might be sufficient, if not more effective.
473    const double kPredictedGrowthFactor = 3.0;
474    final double leftwardTrend = kPredictedGrowthFactor *
475        math.max(oldOptimalLocalCullRect.left - _exactLocalCullRect.left, 0);
476    final double upwardTrend = kPredictedGrowthFactor *
477        math.max(oldOptimalLocalCullRect.top - _exactLocalCullRect.top, 0);
478    final double rightwardTrend = kPredictedGrowthFactor *
479        math.max(_exactLocalCullRect.right - oldOptimalLocalCullRect.right, 0);
480    final double bottomwardTrend = kPredictedGrowthFactor *
481        math.max(
482            _exactLocalCullRect.bottom - oldOptimalLocalCullRect.bottom, 0);
483
484    final ui.Rect newLocalCullRect = ui.Rect.fromLTRB(
485      oldOptimalLocalCullRect.left - leftwardTrend,
486      oldOptimalLocalCullRect.top - upwardTrend,
487      oldOptimalLocalCullRect.right + rightwardTrend,
488      oldOptimalLocalCullRect.bottom + bottomwardTrend,
489    ).intersect(localPaintBounds);
490
491    final bool localCullRectChanged = _optimalLocalCullRect != newLocalCullRect;
492    _optimalLocalCullRect = newLocalCullRect;
493    return localCullRectChanged;
494  }
495
496  /// Number of bitmap pixel painted by this picture.
497  ///
498  /// If the implementation does not paint onto a bitmap canvas, it should
499  /// return zero.
500  int get bitmapPixelCount;
501
502  void _applyPaint(PersistedPicture oldSurface) {
503    final EngineCanvas oldCanvas = oldSurface?._canvas;
504    if (!picture.recordingCanvas.didDraw) {
505      _recycleCanvas(oldCanvas);
506      domRenderer.clearDom(rootElement);
507      return;
508    }
509
510    if (_debugExplainSurfaceStats) {
511      _surfaceStatsFor(this).paintCount++;
512    }
513
514    assert(_optimalLocalCullRect != null);
515    applyPaint(oldCanvas);
516  }
517
518  /// Concrete implementations implement this method to do actual painting.
519  void applyPaint(EngineCanvas oldCanvas);
520
521  void _applyTranslate() {
522    rootElement.style.transform = 'translate(${dx}px, ${dy}px)';
523  }
524
525  @override
526  void apply() {
527    _applyTranslate();
528    _applyPaint(null);
529  }
530
531  @override
532  void build() {
533    _computeOptimalCullRect(null);
534    super.build();
535  }
536
537  @override
538  void update(PersistedPicture oldSurface) {
539    super.update(oldSurface);
540
541    if (dx != oldSurface.dx || dy != oldSurface.dy) {
542      _applyTranslate();
543    }
544
545    final bool cullRectChangeRequiresRepaint =
546        _computeOptimalCullRect(oldSurface);
547    if (identical(picture, oldSurface.picture)) {
548      // The picture is the same. Attempt to avoid repaint.
549      if (cullRectChangeRequiresRepaint) {
550        // Cull rect changed such that a repaint is still necessary.
551        _applyPaint(oldSurface);
552      } else {
553        // Cull rect did not change, or changed such in a way that does not
554        // require a repaint (e.g. it shrunk).
555        _canvas = oldSurface._canvas;
556      }
557    } else {
558      // We have a new picture. Repaint.
559      _applyPaint(oldSurface);
560    }
561  }
562
563  @override
564  void retain() {
565    super.retain();
566    final bool cullRectChangeRequiresRepaint = _computeOptimalCullRect(this);
567    if (cullRectChangeRequiresRepaint) {
568      _applyPaint(this);
569    }
570  }
571
572  @override
573  void discard() {
574    _recycleCanvas(_canvas);
575    super.discard();
576  }
577
578  @override
579  void debugPrintChildren(StringBuffer buffer, int indent) {
580    super.debugPrintChildren(buffer, indent);
581    if (rootElement != null && rootElement.firstChild != null) {
582      final html.Element firstChild = rootElement.firstChild;
583      final String canvasTag = firstChild.tagName.toLowerCase();
584      final int canvasHash = rootElement.firstChild.hashCode;
585      buffer.writeln('${'  ' * (indent + 1)}<$canvasTag @$canvasHash />');
586    } else if (rootElement != null) {
587      buffer.writeln(
588          '${'  ' * (indent + 1)}<${rootElement.tagName.toLowerCase()} @$hashCode />');
589    } else {
590      buffer.writeln('${'  ' * (indent + 1)}<recycled-canvas />');
591    }
592  }
593
594  @override
595  void debugValidate(List<String> validationErrors) {
596    super.debugValidate(validationErrors);
597
598    if (picture.recordingCanvas.didDraw) {
599      if (_canvas == null) {
600        validationErrors
601            .add('$runtimeType has non-trivial picture but it has null canvas');
602      }
603      if (_optimalLocalCullRect == null) {
604        validationErrors.add('$runtimeType has null _optimalLocalCullRect');
605      }
606      if (_exactGlobalCullRect == null) {
607        validationErrors.add('$runtimeType has null _exactGlobalCullRect');
608      }
609      if (_exactLocalCullRect == null) {
610        validationErrors.add('$runtimeType has null _exactLocalCullRect');
611      }
612    }
613  }
614}
615