LegacyTileGrid.mm   [plain text]


/*
 * Copyright (C) 2011 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "LegacyTileGrid.h"

#if PLATFORM(IOS_FAMILY)

#include "LegacyTileGridTile.h"
#include "LegacyTileLayer.h"
#include "LegacyTileLayerPool.h"
#include "SystemMemory.h"
#include "WAKWindow.h"
#include <algorithm>
#include <functional>
#include <pal/spi/cg/CoreGraphicsSPI.h>
#include <pal/spi/cocoa/QuartzCoreSPI.h>
#include <wtf/MemoryPressureHandler.h>

namespace WebCore {

LegacyTileGrid::LegacyTileGrid(LegacyTileCache& tileCache, const IntSize& tileSize)
    : m_tileCache(tileCache)
    , m_tileHostLayer(adoptNS([[LegacyTileHostLayer alloc] initWithTileGrid:this]))
    , m_tileSize(tileSize)
    , m_scale(1)
    , m_validBounds(0, 0, std::numeric_limits<int>::max(), std::numeric_limits<int>::max()) 
{
}
    
LegacyTileGrid::~LegacyTileGrid()
{
    [m_tileHostLayer removeFromSuperlayer];
}

IntRect LegacyTileGrid::visibleRect() const
{
    IntRect visibleRect = enclosingIntRect(m_tileCache.visibleRectInLayer(m_tileHostLayer.get()));

    // When fast scrolling to the top, move the visible rect there immediately so we have tiles when the scrolling completes.
    if (m_tileCache.tilingMode() == LegacyTileCache::ScrollToTop)
        visibleRect.setY(0);

    return visibleRect;
}

void LegacyTileGrid::dropAllTiles()
{
    m_tiles.clear();
}

void LegacyTileGrid::dropTilesIntersectingRect(const IntRect& dropRect)
{
    dropTilesBetweenRects(dropRect, IntRect());
}

void LegacyTileGrid::dropTilesOutsideRect(const IntRect& keepRect)
{
    dropTilesBetweenRects(IntRect(0, 0, std::numeric_limits<int>::max(), std::numeric_limits<int>::max()), keepRect);
}

void LegacyTileGrid::dropTilesBetweenRects(const IntRect& dropRect, const IntRect& keepRect)
{
    Vector<TileIndex> toRemove;
    for (const auto& tile : m_tiles) {
        const TileIndex& index = tile.key;
        IntRect tileRect = tile.value->rect();
        if (tileRect.intersects(dropRect) && !tileRect.intersects(keepRect))
            toRemove.append(index);
    }
    unsigned removeCount = toRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        m_tiles.remove(toRemove[n]);
}

unsigned LegacyTileGrid::tileByteSize() const
{
    IntSize tilePixelSize = m_tileSize;
    tilePixelSize.scale(m_tileCache.screenScale());
    return LegacyTileLayerPool::bytesBackingLayerWithPixelSize(tilePixelSize);
}

template <typename T>
static bool isFartherAway(const std::pair<double, T>& a, const std::pair<double, T>& b)
{
    return a.first > b.first;
}

bool LegacyTileGrid::dropDistantTiles(unsigned tilesNeeded, double shortestDistance)
{
    unsigned bytesPerTile = tileByteSize();
    unsigned bytesNeeded = tilesNeeded * bytesPerTile;
    unsigned bytesUsed = tileCount() * bytesPerTile;
    unsigned maximumBytes = m_tileCache.tileCapacityForGrid(this);

    int bytesToReclaim = int(bytesUsed) - (int(maximumBytes) - bytesNeeded);
    if (bytesToReclaim <= 0)
        return true;

    unsigned tilesToRemoveCount = bytesToReclaim / bytesPerTile;

    IntRect visibleRect = this->visibleRect();
    Vector<std::pair<double, TileIndex>> toRemove;
    for (const auto& tile : m_tiles) {
        const TileIndex& index = tile.key;
        const IntRect& tileRect = tile.value->rect();
        double distance = tileDistance2(visibleRect, tileRect);
        if (distance <= shortestDistance)
            continue;
        toRemove.append(std::make_pair(distance, index));
        ALLOW_DEPRECATED_DECLARATIONS_BEGIN
        std::push_heap(toRemove.begin(), toRemove.end(), isFartherAway<TileIndex>);
        if (toRemove.size() > tilesToRemoveCount) {
            std::pop_heap(toRemove.begin(), toRemove.end(), isFartherAway<TileIndex>);
            toRemove.removeLast();
        }
        ALLOW_DEPRECATED_DECLARATIONS_END
    }
    size_t removeCount = toRemove.size();
    for (size_t n = 0; n < removeCount; ++n)
        m_tiles.remove(toRemove[n].second);

    if (!shortestDistance)
        return true;

    return tileCount() * bytesPerTile + bytesNeeded <= maximumBytes;
}

void LegacyTileGrid::addTilesCoveringRect(const IntRect& rectToCover)
{
    // We never draw anything outside of our bounds.
    IntRect rect(rectToCover);
    rect.intersect(bounds());
    if (rect.isEmpty())
        return;

    TileIndex topLeftIndex = tileIndexForPoint(topLeft(rect));
    TileIndex bottomRightIndex = tileIndexForPoint(bottomRight(rect));
    for (int yIndex = topLeftIndex.y(); yIndex <= bottomRightIndex.y(); ++yIndex) {
        for (int xIndex = topLeftIndex.x(); xIndex <= bottomRightIndex.x(); ++xIndex) {
            TileIndex index(xIndex, yIndex);
            if (!tileForIndex(index))
                addTileForIndex(index);
        }
    }
}

void LegacyTileGrid::addTileForIndex(const TileIndex& index)
{
    m_tiles.set(index, LegacyTileGridTile::create(this, tileRectForIndex(index)));
}

CALayer* LegacyTileGrid::tileHostLayer() const
{
    return m_tileHostLayer.get();
}

IntRect LegacyTileGrid::bounds() const
{
    return IntRect(IntPoint(), IntSize([tileHostLayer() size]));
}

RefPtr<LegacyTileGridTile> LegacyTileGrid::tileForIndex(const TileIndex& index) const
{
    return m_tiles.get(index);
}

IntRect LegacyTileGrid::tileRectForIndex(const TileIndex& index) const
{
    IntRect rect(index.x() * m_tileSize.width() - (m_origin.x() ? m_tileSize.width() - m_origin.x() : 0),
                 index.y() * m_tileSize.height() - (m_origin.y() ? m_tileSize.height() - m_origin.y() : 0),
                 m_tileSize.width(),
                 m_tileSize.height());
    rect.intersect(bounds());
    return rect;
}

LegacyTileGrid::TileIndex LegacyTileGrid::tileIndexForPoint(const IntPoint& point) const
{
    ASSERT(m_origin.x() < m_tileSize.width());
    ASSERT(m_origin.y() < m_tileSize.height());
    int x = (point.x() + (m_origin.x() ? m_tileSize.width() - m_origin.x() : 0)) / m_tileSize.width();
    int y = (point.y() + (m_origin.y() ? m_tileSize.height() - m_origin.y() : 0)) / m_tileSize.height();
    return TileIndex(std::max(x, 0), std::max(y, 0));
}

void LegacyTileGrid::centerTileGridOrigin(const IntRect& visibleRect)
{
    if (visibleRect.isEmpty())
        return;

    unsigned minimumHorizontalTiles = 1 + (visibleRect.width() - 1) / m_tileSize.width();
    unsigned minimumVerticalTiles = 1 + (visibleRect.height() - 1) / m_tileSize.height();
    TileIndex currentTopLeftIndex = tileIndexForPoint(topLeft(visibleRect));
    TileIndex currentBottomRightIndex = tileIndexForPoint(bottomRight(visibleRect));
    unsigned currentHorizontalTiles = currentBottomRightIndex.x() - currentTopLeftIndex.x() + 1;
    unsigned currentVerticalTiles = currentBottomRightIndex.y() - currentTopLeftIndex.y() + 1;

    // If we have tiles already, only center if we would get benefits from both directions (as we need to throw out existing tiles).
    if (tileCount() && (currentHorizontalTiles == minimumHorizontalTiles || currentVerticalTiles == minimumVerticalTiles))
        return;

    IntPoint newOrigin(0, 0);
    IntSize size = bounds().size();
    if (size.width() > m_tileSize.width()) {
        newOrigin.setX((visibleRect.x() - (minimumHorizontalTiles * m_tileSize.width() - visibleRect.width()) / 2) % m_tileSize.width());
        if (newOrigin.x() < 0)
            newOrigin.setX(0);
    }
    if (size.height() > m_tileSize.height()) {
        newOrigin.setY((visibleRect.y() - (minimumVerticalTiles * m_tileSize.height() - visibleRect.height()) / 2) % m_tileSize.height());
        if (newOrigin.y() < 0)
            newOrigin.setY(0);
    }

    // Drop all existing tiles if the origin moved.
    if (newOrigin == m_origin)
        return;
    m_tiles.clear();
    m_origin = newOrigin;
}

RefPtr<LegacyTileGridTile> LegacyTileGrid::tileForPoint(const IntPoint& point) const
{
    return tileForIndex(tileIndexForPoint(point));
}

bool LegacyTileGrid::tilesCover(const IntRect& rect) const
{
    return tileForPoint(rect.location()) && tileForPoint(IntPoint(rect.maxX() - 1, rect.y())) &&
    tileForPoint(IntPoint(rect.x(), rect.maxY() - 1)) && tileForPoint(IntPoint(rect.maxX() - 1, rect.maxY() - 1));
}

void LegacyTileGrid::updateTileOpacity()
{
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it)
        [it->value->tileLayer() setOpaque:m_tileCache.tilesOpaque()];
}

void LegacyTileGrid::updateTileBorderVisibility()
{
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it)
        it->value->showBorder(m_tileCache.tileBordersVisible());
}

unsigned LegacyTileGrid::tileCount() const
{
    return m_tiles.size();
}

bool LegacyTileGrid::checkDoSingleTileLayout()
{
    IntSize size = bounds().size();
    if (size.width() > m_tileSize.width() || size.height() > m_tileSize.height())
        return false;

    if (m_origin != IntPoint(0, 0)) {
        m_tiles.clear();
        m_origin = IntPoint(0, 0);
    }

    dropInvalidTiles();

    if (size.isEmpty()) {
        ASSERT(!m_tiles.get(TileIndex(0, 0)));
        return true;
    }

    TileIndex originIndex(0, 0);
    if (!m_tiles.get(originIndex))
        m_tiles.set(originIndex, LegacyTileGridTile::create(this, tileRectForIndex(originIndex)));

    return true;
}

void LegacyTileGrid::updateHostLayerSize()
{
    CALayer* hostLayer = m_tileCache.hostLayer();
    CGRect tileHostBounds = [hostLayer convertRect:[hostLayer bounds] toLayer:tileHostLayer()];
    CGSize transformedSize;
    transformedSize.width = CGRound(tileHostBounds.size.width);
    transformedSize.height = CGRound(tileHostBounds.size.height);

    CGRect bounds = [tileHostLayer() bounds];
    if (CGSizeEqualToSize(bounds.size, transformedSize))
        return;
    bounds.size = transformedSize;
    [tileHostLayer() setBounds:bounds];
}

void LegacyTileGrid::dropInvalidTiles()
{
    IntRect bounds = this->bounds();
    IntRect dropBounds = intersection(m_validBounds, bounds);
    Vector<TileIndex> toRemove;
    for (const auto& tile : m_tiles) {
        const TileIndex& index = tile.key;
        const IntRect& tileRect = tile.value->rect();
        IntRect expectedTileRect = tileRectForIndex(index);
        if (expectedTileRect != tileRect || !dropBounds.contains(tileRect))
            toRemove.append(index);
    }
    unsigned removeCount = toRemove.size();
    for (unsigned n = 0; n < removeCount; ++n)
        m_tiles.remove(toRemove[n]);

    m_validBounds = bounds;
}

void LegacyTileGrid::invalidateTiles(const IntRect& dirtyRect)
{
    if (!hasTiles())
        return;

    IntRect bounds = this->bounds();
    if (intersection(bounds, m_validBounds) != m_validBounds) {
        // The bounds have got smaller. Everything outside will also be considered invalid and will be dropped by dropInvalidTiles().
        // Due to dirtyRect being limited to current bounds the tiles that are temporarily outside might miss invalidation 
        // completely othwerwise.
        m_validBounds = bounds;
    }

    Vector<TileIndex> invalidatedTiles;

    if (dirtyRect.width() > m_tileSize.width() * 4 || dirtyRect.height() > m_tileSize.height() * 4) {
        // For large invalidates, iterate over live tiles.
        TileMap::iterator end = m_tiles.end();
        for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
            LegacyTileGridTile* tile = it->value.get();
            if (!tile->rect().intersects(dirtyRect))
               continue;
            tile->invalidateRect(dirtyRect);
            invalidatedTiles.append(it->key);
        }
    } else {
        TileIndex topLeftIndex = tileIndexForPoint(topLeft(dirtyRect));
        TileIndex bottomRightIndex = tileIndexForPoint(bottomRight(dirtyRect));
        for (int yIndex = topLeftIndex.y(); yIndex <= bottomRightIndex.y(); ++yIndex) {
            for (int xIndex = topLeftIndex.x(); xIndex <= bottomRightIndex.x(); ++xIndex) {
                TileIndex index(xIndex, yIndex);
                RefPtr<LegacyTileGridTile> tile = tileForIndex(index);
                if (!tile)
                    continue;
                if (!tile->rect().intersects(dirtyRect))
                    continue;
                tile->invalidateRect(dirtyRect);
                invalidatedTiles.append(index);
            }
        }
    }
    if (invalidatedTiles.isEmpty())
        return;
    // When using minimal coverage, drop speculative tiles instead of updating them.
    if (!shouldUseMinimalTileCoverage())
        return;
    if (m_tileCache.tilingMode() != LegacyTileCache::Minimal && m_tileCache.tilingMode() != LegacyTileCache::Normal)
        return;
    IntRect visibleRect = this->visibleRect();
    unsigned count = invalidatedTiles.size();
    for (unsigned i = 0; i < count; ++i) {
        RefPtr<LegacyTileGridTile> tile = tileForIndex(invalidatedTiles[i]);
        if (!tile->rect().intersects(visibleRect))
            m_tiles.remove(invalidatedTiles[i]);
    }
}

bool LegacyTileGrid::shouldUseMinimalTileCoverage() const
{
    return m_tileCache.tilingMode() == LegacyTileCache::Minimal
        || !m_tileCache.isSpeculativeTileCreationEnabled()
        || MemoryPressureHandler::singleton().isUnderMemoryPressure();
}

IntRect LegacyTileGrid::adjustCoverRectForPageBounds(const IntRect& rect) const
{
    // Adjust the rect so that it stays within the bounds and keeps the pixel size.
    IntRect bounds = this->bounds();
    IntRect adjustedRect = rect;
    adjustedRect.move(rect.x() < bounds.x() ? bounds.x() - rect.x() : 0,
              rect.y() < bounds.y() ? bounds.y() - rect.y() : 0);
    adjustedRect.move(rect.maxX() > bounds.maxX() ? bounds.maxX() - rect.maxX() : 0,
              rect.maxY() > bounds.maxY() ? bounds.maxY() - rect.maxY() : 0);
    adjustedRect = intersection(bounds, adjustedRect);
    if (adjustedRect == rect || adjustedRect.isEmpty() || shouldUseMinimalTileCoverage())
        return adjustedRect;
    int pixels = adjustedRect.width() * adjustedRect.height();
    if (adjustedRect.width() != rect.width())
        adjustedRect.inflateY((pixels / adjustedRect.width() - adjustedRect.height()) / 2);
    else if (adjustedRect.height() != rect.height())
        adjustedRect.inflateX((pixels / adjustedRect.height() - adjustedRect.width()) / 2);
    return intersection(adjustedRect, bounds);
}

IntRect LegacyTileGrid::calculateCoverRect(const IntRect& visibleRect, bool& centerGrid)
{
    // Use minimum coverRect if we are under memory pressure.
    if (shouldUseMinimalTileCoverage()) {
        centerGrid = true;
        return visibleRect;
    }
    IntRect coverRect = visibleRect;
    centerGrid = false;
    coverRect.inflateX(visibleRect.width() / 2);
    coverRect.inflateY(visibleRect.height());
    return adjustCoverRectForPageBounds(coverRect);
}

double LegacyTileGrid::tileDistance2(const IntRect& visibleRect, const IntRect& tileRect) const
{
    // The "distance" calculated here is used to pick which tile to cache next. The idea is to create those
    // closest to the current viewport first so the user is more likely to see already rendered content we she
    // scrolls. The calculation is weighted to prefer vertical and downward direction.
    if (visibleRect.intersects(tileRect))
        return 0;
    IntPoint visibleCenter = visibleRect.location() + IntSize(visibleRect.width() / 2, visibleRect.height() / 2);
    IntPoint tileCenter = tileRect.location() + IntSize(tileRect.width() / 2, tileRect.height() / 2);
    
    double horizontalBias = 1.0;
    double leftwardBias = 1.0;
    double rightwardBias = 1.0;

    double verticalBias = 1.0;
    double upwardBias = 1.0;
    double downwardBias = 1.0;

    const double tilingBiasVeryLikely = 0.8;
    const double tilingBiasLikely = 0.9;

    switch (m_tileCache.tilingDirection()) {
    case LegacyTileCache::TilingDirectionUp:
        verticalBias = tilingBiasVeryLikely;
        upwardBias = tilingBiasLikely;
        break;
    case LegacyTileCache::TilingDirectionDown:
        verticalBias = tilingBiasVeryLikely;
        downwardBias = tilingBiasLikely;
        break;
    case LegacyTileCache::TilingDirectionLeft:
        horizontalBias = tilingBiasVeryLikely;
        leftwardBias = tilingBiasLikely;
        break;
    case LegacyTileCache::TilingDirectionRight:
        horizontalBias = tilingBiasVeryLikely;
        rightwardBias = tilingBiasLikely;
        break;
    }

    double xScale = horizontalBias * visibleRect.height() / visibleRect.width() * (tileCenter.x() >= visibleCenter.x() ? rightwardBias : leftwardBias);
    double yScale = verticalBias * visibleRect.width() / visibleRect.height() * (tileCenter.y() >= visibleCenter.y() ? downwardBias : upwardBias);

    double xDistance = xScale * (tileCenter.x() - visibleCenter.x());
    double yDistance = yScale * (tileCenter.y() - visibleCenter.y());

    double distance2 = xDistance * xDistance + yDistance * yDistance;
    return distance2;
}

void LegacyTileGrid::createTiles(LegacyTileCache::SynchronousTileCreationMode creationMode)
{
    IntRect visibleRect = this->visibleRect();
    if (visibleRect.isEmpty())
        return;

    // Drop tiles that are wrong size or outside the frame (because the frame has been resized).
    dropInvalidTiles();

    bool centerGrid;
    IntRect coverRect = calculateCoverRect(visibleRect, centerGrid);

    // If tile size is bigger than the view, centering minimizes the painting needed to cover the screen.
    // This is especially useful after zooming 
    centerGrid = centerGrid || !tileCount();
    if (centerGrid)
        centerTileGridOrigin(visibleRect);

    double shortestDistance = std::numeric_limits<double>::infinity();
    double coveredDistance = 0;
    Vector<LegacyTileGrid::TileIndex> tilesToCreate;
    unsigned pendingTileCount = 0;

    LegacyTileGrid::TileIndex topLeftIndex = tileIndexForPoint(topLeft(coverRect));
    LegacyTileGrid::TileIndex bottomRightIndex = tileIndexForPoint(bottomRight(coverRect));
    for (int yIndex = topLeftIndex.y(); yIndex <= bottomRightIndex.y(); ++yIndex) {
        for (int xIndex = topLeftIndex.x(); xIndex <= bottomRightIndex.x(); ++xIndex) {
            LegacyTileGrid::TileIndex index(xIndex, yIndex);
            // Currently visible tiles have distance of 0 and get all created in the same transaction.
            double distance = tileDistance2(visibleRect, tileRectForIndex(index));
            if (distance > coveredDistance)
                coveredDistance = distance;
            if (tileForIndex(index))
                continue;
            ++pendingTileCount;
            if (distance > shortestDistance)
                continue;
            if (distance < shortestDistance) {
                tilesToCreate.clear();
                shortestDistance = distance;
            }
            tilesToCreate.append(index);
        }
    }

    size_t tilesToCreateCount = tilesToCreate.size();

    // Tile creation timer will invoke this function again in CoverSpeculative mode.
    bool candidateTilesAreSpeculative = shortestDistance > 0;
    if (creationMode == LegacyTileCache::CoverVisibleOnly && candidateTilesAreSpeculative)
        tilesToCreateCount = 0;

    // Even if we don't create any tiles, we should still drop distant tiles
    // in case coverRect got smaller.
    double keepDistance = std::min(shortestDistance, coveredDistance);
    if (!dropDistantTiles(tilesToCreateCount, keepDistance))
        return;

    ASSERT(pendingTileCount >= tilesToCreateCount);
    if (!pendingTileCount)
        return;

    for (size_t n = 0; n < tilesToCreateCount; ++n)
        addTileForIndex(tilesToCreate[n]);

    bool didCreateTiles = !!tilesToCreateCount;
    bool createMoreTiles = pendingTileCount > tilesToCreateCount;
    m_tileCache.finishedCreatingTiles(didCreateTiles, createMoreTiles);
}

void LegacyTileGrid::dumpTiles()
{
    IntRect visibleRect = this->visibleRect();
    NSLog(@"transformed visibleRect = [%6d %6d %6d %6d]", visibleRect.x(), visibleRect.y(), visibleRect.width(), visibleRect.height());
    unsigned i = 0;
    TileMap::iterator end = m_tiles.end();
    for (TileMap::iterator it = m_tiles.begin(); it != end; ++it) {
        TileIndex& index = it->key;
        IntRect tileRect = it->value->rect();
        NSLog(@"#%-3d (%3d %3d) - [%6d %6d %6d %6d]%@", ++i, index.x(), index.y(), tileRect.x(), tileRect.y(), tileRect.width(), tileRect.height(), tileRect.intersects(visibleRect) ? @" *" : @"");
        NSLog(@"     %@", [it->value->tileLayer() contents]);
    }
}

} // namespace WebCore

#endif // PLATFORM(IOS_FAMILY)