| // 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. |
| |
| // @dart = 2.10 |
| part of engine; |
| |
| // TODO(yjbanov): this is currently very naive. We probably want to cache |
| // fewer large canvases than small canvases. We could also |
| // improve cache hit count if we did not require exact canvas |
| // size match, but instead could choose a canvas that's big |
| // enough. The optimal heuristic will need to be figured out. |
| // For example, we probably don't want to pick a full-screen |
| // canvas to draw a 10x10 picture. Let's revisit this after |
| // Harry's layer merging refactor. |
| /// The maximum number canvases cached. |
| const int _kCanvasCacheSize = 30; |
| |
| /// Canvases available for reuse, capped at [_kCanvasCacheSize]. |
| final List<BitmapCanvas> _recycledCanvases = <BitmapCanvas>[]; |
| |
| /// Reduces recycled canvas list by 50% to reduce bitmap canvas memory use. |
| void _reduceCanvasMemoryUsage() { |
| final int canvasCount = _recycledCanvases.length; |
| for (int i = 0; i < canvasCount; i++) { |
| _recycledCanvases[i].dispose(); |
| } |
| _recycledCanvases.clear(); |
| } |
| |
| /// A request to repaint a canvas. |
| /// |
| /// Paint requests are prioritized such that the larger pictures go first. This |
| /// makes canvas allocation more efficient by letting large pictures claim |
| /// larger recycled canvases. Otherwise, small pictures would claim the large |
| /// canvases forcing us to allocate new large canvases. |
| class _PaintRequest { |
| _PaintRequest({ |
| required this.canvasSize, |
| required this.paintCallback, |
| }) : assert(canvasSize != null), // ignore: unnecessary_null_comparison |
| assert(paintCallback != null); // ignore: unnecessary_null_comparison |
| |
| final ui.Size canvasSize; |
| final ui.VoidCallback paintCallback; |
| } |
| |
| /// Repaint requests produced by [PersistedPicture]s that actually paint on the |
| /// canvas. Painting is delayed until the layer tree is updated to maximize |
| /// the number of reusable canvases. |
| List<_PaintRequest> _paintQueue = <_PaintRequest>[]; |
| |
| void _recycleCanvas(EngineCanvas? canvas) { |
| if (canvas is BitmapCanvas) { |
| canvas.setElementCache(null); |
| if (canvas.isReusable()) { |
| _recycledCanvases.add(canvas); |
| if (_recycledCanvases.length > _kCanvasCacheSize) { |
| final BitmapCanvas removedCanvas = _recycledCanvases.removeAt(0); |
| removedCanvas.dispose(); |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.disposedCount++; |
| } |
| } |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.inRecycleCount = |
| _recycledCanvases.length; |
| } |
| } else { |
| canvas.dispose(); |
| } |
| } |
| } |
| |
| /// A surface that uses a combination of `<canvas>`, `<div>` and `<p>` elements |
| /// to draw shapes and text. |
| class PersistedPicture extends PersistedLeafSurface { |
| PersistedPicture(this.dx, this.dy, this.picture, this.hints) |
| : localPaintBounds = picture.recordingCanvas!.pictureBounds; |
| |
| EngineCanvas? _canvas; |
| |
| /// Returns the canvas used by this picture layer. |
| /// |
| /// Useful for tests. |
| EngineCanvas? get debugCanvas => _canvas; |
| |
| final double dx; |
| final double dy; |
| final EnginePicture picture; |
| final ui.Rect? localPaintBounds; |
| final int hints; |
| double _density = 1.0; |
| /// Cull rect changes and density changes due to transforms should |
| /// call applyPaint for picture when retain() or update() is called after |
| /// preroll is complete. |
| bool _requiresRepaint = false; |
| |
| /// Cache for reusing elements such as images across picture updates. |
| CrossFrameCache<html.HtmlElement>? _elementCache = |
| CrossFrameCache<html.HtmlElement>(); |
| |
| @override |
| html.Element createElement() { |
| return defaultCreateElement('flt-picture'); |
| } |
| |
| @override |
| void recomputeTransformAndClip() { |
| _transform = parent!._transform; |
| if (dx != 0.0 || dy != 0.0) { |
| _transform = _transform!.clone(); |
| _transform!.translate(dx, dy); |
| } |
| final double paintWidth = localPaintBounds!.width; |
| final double paintHeight = localPaintBounds!.height; |
| final double newDensity = localPaintBounds == null || paintWidth == 0 || paintHeight == 0 |
| ? 1.0 : _computePixelDensity(_transform, paintWidth, paintHeight); |
| if (newDensity != _density) { |
| _density = newDensity; |
| _requiresRepaint = true; |
| } |
| _computeExactCullRects(); |
| } |
| |
| /// The rectangle that contains all visible pixels drawn by [picture] inside |
| /// the current layer hierarchy in local coordinates. |
| /// |
| /// This value is a conservative estimate, i.e. it must be big enough to |
| /// contain everything that's visible, but it may be bigger than necessary. |
| /// Therefore it should not be used for clipping. It is meant to be used for |
| /// optimizing canvas allocation. |
| ui.Rect? get optimalLocalCullRect => _optimalLocalCullRect; |
| ui.Rect? _optimalLocalCullRect; |
| |
| /// Same as [optimalLocalCullRect] but in screen coordinate system. |
| ui.Rect? get debugExactGlobalCullRect => _exactGlobalCullRect; |
| ui.Rect? _exactGlobalCullRect; |
| |
| ui.Rect? _exactLocalCullRect; |
| |
| /// Computes the canvas paint bounds based on the estimated paint bounds and |
| /// the scaling produced by transformations. |
| /// |
| /// Return `true` if the local cull rect changed, indicating that a repaint |
| /// may be required. Returns `false` otherwise. Global cull rect changes do |
| /// not necessarily incur repaints. For example, if the layer sub-tree was |
| /// translated from one frame to another we may not need to repaint, just |
| /// translate the canvas. |
| void _computeExactCullRects() { |
| assert(transform != null); |
| assert(localPaintBounds != null); |
| |
| if (parent!._projectedClip == null) { |
| // Compute and cache chain of clipping bounds on parent of picture since |
| // parent may include multiple pictures so it can be reused by all |
| // child pictures. |
| ui.Rect? bounds; |
| PersistedSurface? parentSurface = parent; |
| final Matrix4 clipTransform = Matrix4.identity(); |
| while (parentSurface != null) { |
| final ui.Rect? localClipBounds = parentSurface._localClipBounds; |
| if (localClipBounds != null) { |
| if (bounds == null) { |
| bounds = transformRect(clipTransform, localClipBounds); |
| } else { |
| bounds = |
| bounds.intersect(transformRect(clipTransform, localClipBounds)); |
| } |
| } |
| final Matrix4? localInverse = parentSurface.localTransformInverse; |
| if (localInverse != null && !localInverse.isIdentity()) { |
| clipTransform.multiply(localInverse); |
| } |
| parentSurface = parentSurface.parent; |
| } |
| if (bounds != null && (bounds.width <= 0 || bounds.height <= 0)) { |
| bounds = ui.Rect.zero; |
| } |
| // Cache projected clip on parent. |
| parent!._projectedClip = bounds; |
| } |
| // Intersect localPaintBounds with parent projected clip to calculate |
| // and cache [_exactLocalCullRect]. |
| if (parent!._projectedClip == null) { |
| _exactLocalCullRect = localPaintBounds; |
| } else { |
| _exactLocalCullRect = |
| localPaintBounds!.intersect(parent!._projectedClip!); |
| } |
| if (_exactLocalCullRect!.width <= 0 || _exactLocalCullRect!.height <= 0) { |
| _exactLocalCullRect = ui.Rect.zero; |
| _exactGlobalCullRect = ui.Rect.zero; |
| } else { |
| assert(() { |
| _exactGlobalCullRect = transformRect(transform!, _exactLocalCullRect!); |
| return true; |
| }()); |
| } |
| } |
| |
| void _computeOptimalCullRect(PersistedPicture? oldSurface) { |
| assert(_exactLocalCullRect != null); |
| |
| if (oldSurface == null || !oldSurface.picture.recordingCanvas!.didDraw) { |
| // First useful paint. |
| _optimalLocalCullRect = _exactLocalCullRect; |
| _requiresRepaint = true; |
| return; |
| } |
| |
| assert(oldSurface._optimalLocalCullRect != null); |
| |
| final bool surfaceBeingRetained = identical(oldSurface, this); |
| final ui.Rect? oldOptimalLocalCullRect = surfaceBeingRetained |
| ? _optimalLocalCullRect |
| : oldSurface._optimalLocalCullRect; |
| |
| if (_exactLocalCullRect == ui.Rect.zero) { |
| // The clip collapsed into a zero-sized rectangle. If it was already zero, |
| // no need to signal cull rect change. |
| _optimalLocalCullRect = ui.Rect.zero; |
| if (oldOptimalLocalCullRect != ui.Rect.zero) { |
| _requiresRepaint = true; |
| } |
| return; |
| } |
| |
| if (rectContainsOther(oldOptimalLocalCullRect!, _exactLocalCullRect!)) { |
| // The cull rect we computed in the past contains the newly computed cull |
| // rect. This can happen, for example, when the picture is being shrunk by |
| // a clip when it is scrolled out of the screen. In this case we do not |
| // repaint the picture. We just let it be shrunk by the outer clip. |
| _optimalLocalCullRect = oldOptimalLocalCullRect; |
| return; |
| } |
| |
| // The new cull rect contains area not covered by a previous rect. Perhaps |
| // the clip is growing, moving around the picture, or both. In this case |
| // a part of the picture may not have been painted. We will need to |
| // request a new canvas and paint the picture on it. However, this is also |
| // a strong signal that the clip will continue growing as typically |
| // Flutter uses animated transitions. So instead of allocating the canvas |
| // the size of the currently visible area, we try to allocate a canvas of |
| // a bigger size. This will prevent any further repaints as future frames |
| // will hit the above case where the new cull rect is fully contained |
| // within the cull rect we compute now. |
| |
| // Compute the delta, by which each of the side of the clip rect has "moved" |
| // since the last time we updated the cull rect. |
| final double leftwardDelta = |
| oldOptimalLocalCullRect.left - _exactLocalCullRect!.left; |
| final double upwardDelta = |
| oldOptimalLocalCullRect.top - _exactLocalCullRect!.top; |
| final double rightwardDelta = |
| _exactLocalCullRect!.right - oldOptimalLocalCullRect.right; |
| final double bottomwardDelta = |
| _exactLocalCullRect!.bottom - oldOptimalLocalCullRect.bottom; |
| |
| // Compute the new optimal rect to paint into. |
| final ui.Rect newLocalCullRect = ui.Rect.fromLTRB( |
| _exactLocalCullRect!.left - |
| _predictTrend(leftwardDelta, _exactLocalCullRect!.width), |
| _exactLocalCullRect!.top - |
| _predictTrend(upwardDelta, _exactLocalCullRect!.height), |
| _exactLocalCullRect!.right + |
| _predictTrend(rightwardDelta, _exactLocalCullRect!.width), |
| _exactLocalCullRect!.bottom + |
| _predictTrend(bottomwardDelta, _exactLocalCullRect!.height), |
| ).intersect(localPaintBounds!); |
| |
| _requiresRepaint = _optimalLocalCullRect != newLocalCullRect; |
| _optimalLocalCullRect = newLocalCullRect; |
| } |
| |
| /// Predicts the delta a particular side of a clip rect will move given the |
| /// [delta] it moved by last, and the respective [extent] (width or height) |
| /// of the clip. |
| static double _predictTrend(double delta, double extent) { |
| if (delta <= 0.0) { |
| // Shrinking. Give it 10% of the extent in case the trend is reversed. |
| return extent * 0.1; |
| } else { |
| // Growing. Predict 10 more frames of similar deltas. Give it at least |
| // 50% of the extent (protect from extremely slow growth trend such as |
| // slow scrolling). Give no more than the full extent (protects from |
| // fast scrolling that could lead to overallocation). |
| return math.min( |
| math.max(extent * 0.5, delta * 10.0), |
| extent, |
| ); |
| } |
| } |
| |
| /// Number of bitmap pixel painted by this picture. |
| /// |
| /// If the implementation does not paint onto a bitmap canvas, it should |
| /// return zero. |
| int get bitmapPixelCount { |
| if (_canvas is! BitmapCanvas) { |
| return 0; |
| } |
| |
| final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; |
| return bitmapCanvas.bitmapPixelCount; |
| } |
| |
| void _applyPaint(PersistedPicture? oldSurface) { |
| final EngineCanvas? oldCanvas = oldSurface?._canvas; |
| _requiresRepaint = false; |
| if (!picture.recordingCanvas!.didDraw || _optimalLocalCullRect!.isEmpty) { |
| // The picture is empty, or it has been completely clipped out. Skip |
| // painting. This removes all the setup work and scaffolding objects |
| // that won't be useful for anything anyway. |
| _recycleCanvas(oldCanvas); |
| if (rootElement != null) { |
| domRenderer.clearDom(rootElement!); |
| } |
| if (_canvas != null) { |
| _recycleCanvas(_canvas); |
| } |
| _canvas = null; |
| return; |
| } |
| |
| if (_debugExplainSurfaceStats) { |
| _surfaceStatsFor(this).paintCount++; |
| } |
| |
| assert(_optimalLocalCullRect != null); |
| applyPaint(oldCanvas); |
| } |
| |
| @override |
| double matchForUpdate(PersistedPicture existingSurface) { |
| if (existingSurface.picture == picture) { |
| // Picture is the same, return perfect score. |
| return 0.0; |
| } |
| |
| if (!existingSurface.picture.recordingCanvas!.didDraw) { |
| // The previous surface didn't draw anything and therefore has no |
| // resources to reuse. |
| return 1.0; |
| } |
| |
| final bool didRequireBitmap = |
| existingSurface.picture.recordingCanvas!.hasArbitraryPaint; |
| final bool requiresBitmap = picture.recordingCanvas!.hasArbitraryPaint; |
| if (didRequireBitmap != requiresBitmap) { |
| // Switching canvas types is always expensive. |
| return 1.0; |
| } else if (!requiresBitmap) { |
| // Currently DomCanvas is always expensive to repaint, as we always throw |
| // out all the DOM we rendered before. This may change in the future, at |
| // which point we may return other values here. |
| return 1.0; |
| } else { |
| final BitmapCanvas? oldCanvas = existingSurface._canvas as BitmapCanvas?; |
| if (oldCanvas == null) { |
| // We did not allocate a canvas last time. This can happen when the |
| // picture is completely clipped out of the view. |
| return 1.0; |
| } else if (!oldCanvas.doesFitBounds(_exactLocalCullRect!, _density)) { |
| // The canvas needs to be resized before painting. |
| return 1.0; |
| } else { |
| final int newPixelCount = |
| BitmapCanvas._widthToPhysical(_exactLocalCullRect!.width) * |
| BitmapCanvas._heightToPhysical(_exactLocalCullRect!.height); |
| final int oldPixelCount = |
| oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; |
| |
| if (oldPixelCount == 0) { |
| return 1.0; |
| } |
| |
| final double pixelCountRatio = newPixelCount / oldPixelCount; |
| assert(0 <= pixelCountRatio && pixelCountRatio <= 1.0, |
| 'Invalid pixel count ratio $pixelCountRatio'); |
| return 1.0 - pixelCountRatio; |
| } |
| } |
| } |
| |
| @override |
| Matrix4? get localTransformInverse => null; |
| |
| void applyPaint(EngineCanvas? oldCanvas) { |
| if (picture.recordingCanvas!.hasArbitraryPaint) { |
| _applyBitmapPaint(oldCanvas); |
| } else { |
| _applyDomPaint(oldCanvas); |
| } |
| } |
| |
| void _applyDomPaint(EngineCanvas? oldCanvas) { |
| _recycleCanvas(oldCanvas); |
| _canvas = DomCanvas(); |
| domRenderer.clearDom(rootElement!); |
| rootElement!.append(_canvas!.rootElement); |
| picture.recordingCanvas!.apply(_canvas!, _optimalLocalCullRect); |
| } |
| |
| void _applyBitmapPaint(EngineCanvas? oldCanvas) { |
| if (oldCanvas is BitmapCanvas && |
| oldCanvas.doesFitBounds(_optimalLocalCullRect!, _density) && |
| oldCanvas.isReusable()) { |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.keptCount++; |
| } |
| // Re-use old bitmap canvas. |
| oldCanvas.bounds = _optimalLocalCullRect!; |
| _canvas = oldCanvas; |
| oldCanvas.setElementCache(_elementCache); |
| _canvas!.clear(); |
| picture.recordingCanvas!.apply(_canvas!, _optimalLocalCullRect); |
| } else { |
| // We can't use the old canvas because the size has changed, so we put |
| // it in a cache for later reuse. |
| _recycleCanvas(oldCanvas); |
| if (_canvas is BitmapCanvas) { |
| (_canvas as BitmapCanvas).setElementCache(null); |
| } |
| _canvas = null; |
| // We cannot paint immediately because not all canvases that we may be |
| // able to reuse have been released yet. So instead we enqueue this |
| // picture to be painted after the update cycle is done syncing the layer |
| // tree then reuse canvases that were freed up. |
| _paintQueue.add(_PaintRequest( |
| canvasSize: _optimalLocalCullRect!.size, |
| paintCallback: () { |
| _canvas = _findOrCreateCanvas(_optimalLocalCullRect!); |
| if (_canvas is BitmapCanvas) { |
| (_canvas as BitmapCanvas).setElementCache(_elementCache); |
| } |
| if (_debugExplainSurfaceStats) { |
| final BitmapCanvas bitmapCanvas = _canvas as BitmapCanvas; |
| _surfaceStatsFor(this).paintPixelCount += |
| bitmapCanvas.bitmapPixelCount; |
| } |
| domRenderer.clearDom(rootElement!); |
| rootElement!.append(_canvas!.rootElement); |
| _canvas!.clear(); |
| picture.recordingCanvas!.apply(_canvas!, _optimalLocalCullRect); |
| }, |
| )); |
| } |
| } |
| |
| /// Attempts to reuse a canvas from the [_recycledCanvases]. Allocates a new |
| /// one if unable to reuse. |
| /// |
| /// The best recycled canvas is one that: |
| /// |
| /// - Fits the requested [canvasSize]. This is a hard requirement. Otherwise |
| /// we risk clipping the picture. |
| /// - Is the smallest among all possible reusable canvases. This makes canvas |
| /// reuse more efficient. |
| /// - Contains no more than twice the number of requested pixels. This makes |
| /// sure we do not use too much memory for small canvases. |
| BitmapCanvas _findOrCreateCanvas(ui.Rect bounds) { |
| final ui.Size canvasSize = bounds.size; |
| BitmapCanvas? bestRecycledCanvas; |
| double lastPixelCount = double.infinity; |
| for (int i = 0; i < _recycledCanvases.length; i++) { |
| final BitmapCanvas candidate = _recycledCanvases[i]; |
| if (!candidate.isReusable()) { |
| continue; |
| } |
| |
| final ui.Size candidateSize = candidate.size; |
| final double candidatePixelCount = |
| candidateSize.width * candidateSize.height; |
| |
| final bool fits = candidate.doesFitBounds(bounds, _density); |
| final bool isSmaller = candidatePixelCount < lastPixelCount; |
| if (fits && isSmaller) { |
| // [isTooSmall] is used to make sure that a small picture doesn't |
| // reuse and hold onto memory of a large canvas. |
| final double requestedPixelCount = bounds.width * bounds.height; |
| final bool isTooSmall = isSmaller && |
| requestedPixelCount > 1 && |
| (candidatePixelCount / requestedPixelCount) > 4; |
| if (!isTooSmall) { |
| bestRecycledCanvas = candidate; |
| lastPixelCount = candidatePixelCount; |
| final bool fitsExactly = candidateSize.width == canvasSize.width && |
| candidateSize.height == canvasSize.height; |
| if (fitsExactly) { |
| // No need to keep looking any more. |
| break; |
| } |
| } |
| } |
| } |
| |
| if (bestRecycledCanvas != null) { |
| if (_debugExplainSurfaceStats) { |
| _surfaceStatsFor(this).reuseCanvasCount++; |
| } |
| _recycledCanvases.remove(bestRecycledCanvas); |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.inRecycleCount = |
| _recycledCanvases.length; |
| } |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.reusedCount++; |
| } |
| bestRecycledCanvas.bounds = bounds; |
| bestRecycledCanvas.setElementCache(_elementCache); |
| return bestRecycledCanvas; |
| } |
| |
| if (_debugShowCanvasReuseStats) { |
| DebugCanvasReuseOverlay.instance.createdCount++; |
| } |
| final BitmapCanvas canvas = BitmapCanvas(bounds, density: _density); |
| canvas.setElementCache(_elementCache); |
| if (_debugExplainSurfaceStats) { |
| _surfaceStatsFor(this) |
| ..allocateBitmapCanvasCount += 1 |
| ..allocatedBitmapSizeInPixels = |
| canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; |
| } |
| return canvas; |
| } |
| |
| void _applyTranslate() { |
| rootElement!.style.transform = 'translate(${dx}px, ${dy}px)'; |
| } |
| |
| @override |
| void apply() { |
| _applyTranslate(); |
| _applyPaint(null); |
| } |
| |
| @override |
| void build() { |
| _computeOptimalCullRect(null); |
| _requiresRepaint = true; |
| super.build(); |
| } |
| |
| @override |
| void update(PersistedPicture oldSurface) { |
| super.update(oldSurface); |
| // Transfer element cache over. |
| _elementCache = oldSurface._elementCache; |
| if (oldSurface != this) { |
| oldSurface._elementCache = null; |
| } |
| |
| if (dx != oldSurface.dx || dy != oldSurface.dy) { |
| _applyTranslate(); |
| } |
| |
| _computeOptimalCullRect(oldSurface); |
| if (identical(picture, oldSurface.picture)) { |
| bool densityChanged = |
| (_canvas is BitmapCanvas && |
| _density != (_canvas as BitmapCanvas)._density); |
| |
| // The picture is the same. Attempt to avoid repaint. |
| if (_requiresRepaint || densityChanged) { |
| // Cull rect changed such that a repaint is still necessary. |
| _applyPaint(oldSurface); |
| } else { |
| // Cull rect did not change, or changed such in a way that does not |
| // require a repaint (e.g. it shrunk). |
| _canvas = oldSurface._canvas; |
| } |
| } else { |
| // We have a new picture. Repaint. |
| _applyPaint(oldSurface); |
| } |
| } |
| |
| @override |
| void retain() { |
| super.retain(); |
| _computeOptimalCullRect(this); |
| if (_requiresRepaint) { |
| _applyPaint(this); |
| } |
| } |
| |
| @override |
| void discard() { |
| _recycleCanvas(_canvas); |
| super.discard(); |
| } |
| |
| @override |
| void debugPrintChildren(StringBuffer buffer, int indent) { |
| super.debugPrintChildren(buffer, indent); |
| if (rootElement != null && rootElement!.firstChild != null) { |
| final html.Element firstChild = rootElement!.firstChild as html.Element; |
| final String canvasTag = firstChild.tagName.toLowerCase(); |
| final int canvasHash = rootElement!.firstChild!.hashCode; |
| buffer.writeln('${' ' * (indent + 1)}<$canvasTag @$canvasHash />'); |
| } else if (rootElement != null) { |
| buffer.writeln( |
| '${' ' * (indent + 1)}<${rootElement!.tagName.toLowerCase()} @$hashCode />'); |
| } else { |
| buffer.writeln('${' ' * (indent + 1)}<recycled-canvas />'); |
| } |
| } |
| |
| @override |
| void debugValidate(List<String> validationErrors) { |
| super.debugValidate(validationErrors); |
| |
| if (picture.recordingCanvas!.didDraw) { |
| if (!_optimalLocalCullRect!.isEmpty && debugCanvas == null) { |
| validationErrors |
| .add('$runtimeType has non-trivial picture but it has null canvas'); |
| } |
| if (_optimalLocalCullRect == null) { |
| validationErrors.add('$runtimeType has null _optimalLocalCullRect'); |
| } |
| if (_exactGlobalCullRect == null) { |
| validationErrors.add('$runtimeType has null _exactGlobalCullRect'); |
| } |
| if (_exactLocalCullRect == null) { |
| validationErrors.add('$runtimeType has null _exactLocalCullRect'); |
| } |
| } |
| } |
| } |
| |
| /// Given size of a rectangle and transform, computes pixel density |
| /// (scale factor). |
| double _computePixelDensity(Matrix4? transform, double width, double height) { |
| if (transform == null || transform.isIdentity()) { |
| return 1.0; |
| } |
| final Float32List m = transform.storage; |
| // Apply perspective transform to all 4 corners. Can't use left,top, bottom, |
| // right since for example rotating 45 degrees would yield inaccurate size. |
| double minX = m[12] * m[15]; |
| double minY = m[13] * m[15]; |
| double maxX = minX; |
| double maxY = minY; |
| double x = width; |
| double y = height; |
| double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); |
| double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; |
| double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; |
| minX = math.min(minX, xp); |
| maxX = math.max(maxX, xp); |
| minY = math.min(minY, yp); |
| maxY = math.max(maxY, yp); |
| x = 0; |
| wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); |
| xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; |
| yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; |
| minX = math.min(minX, xp); |
| maxX = math.max(maxX, xp); |
| minY = math.min(minY, yp); |
| maxY = math.max(maxY, yp); |
| x = width; |
| y = 0; |
| wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); |
| xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; |
| yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; |
| minX = math.min(minX, xp); |
| maxX = math.max(maxX, xp); |
| minY = math.min(minY, yp); |
| maxY = math.max(maxY, yp); |
| double scaleX = (maxX - minX) / width; |
| double scaleY = (maxY - minY) / height; |
| double scale = math.min(scaleX, scaleY); |
| // kEpsilon guards against divide by zero below. |
| if (scale < kEpsilon || scale == 1) { |
| // Handle local paint bounds scaled to 0, typical when using |
| // transform animations and nothing is drawn. |
| return 1.0; |
| } |
| if (scale > 1) { |
| // Normalize scale to multiples of 2: 1x, 2x, 4x, 6x, 8x. |
| // This is to prevent frequent rescaling of canvas during animations. |
| // |
| // On a fullscreen high dpi device dpi*density*resolution will demand |
| // too much memory, so clamp at 4. |
| scale = math.min(4.0, ((scale / 2.0).ceil() * 2.0)); |
| // Guard against webkit absolute limit. |
| const double kPixelLimit = 1024 * 1024 * 4; |
| if ((width * height * scale * scale) > kPixelLimit && scale > 2) { |
| scale = (kPixelLimit * 0.8) / (width * height); |
| } |
| } else { |
| scale = math.max(2.0 / (2.0 / scale).floor(), 0.0001); |
| } |
| return scale; |
| } |