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