/* * Copyright (C) 2011, 2012, 2013 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. */ #import "config.h" #import "TileController.h" #import "IntRect.h" #import "PlatformCALayer.h" #import "Region.h" #if !PLATFORM(IOS) #import "LayerPool.h" #endif #import "WebLayer.h" #import "WebTiledBackingLayer.h" #import "WebTileLayer.h" #import <QuartzCore/QuartzCore.h> #import <wtf/MainThread.h> #import <WebCore/BlockExceptions.h> #import <utility> using namespace std; #if PLATFORM(IOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 @interface CALayer (WebCALayerDetails) - (void)setAcceleratesDrawing:(BOOL)flag; @end #endif @interface WebTiledScrollingIndicatorLayer : CALayer { WebCore::TileController* _tileController; CALayer *_visibleRectFrameLayer; // Owned by being a sublayer. } @property (assign) WebCore::TileController* tileController; @property (assign) CALayer* visibleRectFrameLayer; @end @implementation WebTiledScrollingIndicatorLayer @synthesize tileController = _tileController; @synthesize visibleRectFrameLayer = _visibleRectFrameLayer; - (id)init { if ((self = [super init])) { [self setStyle:[NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"bounds", [NSNull null], @"position", [NSNull null], @"contents", nil] forKey:@"actions"]]; _visibleRectFrameLayer = [CALayer layer]; [_visibleRectFrameLayer setStyle:[NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], @"bounds", [NSNull null], @"position", [NSNull null], @"borderColor", nil] forKey:@"actions"]]; [self addSublayer:_visibleRectFrameLayer]; [_visibleRectFrameLayer setBorderColor:WebCore::cachedCGColor(WebCore::Color(255, 0, 0), WebCore::ColorSpaceDeviceRGB)]; [_visibleRectFrameLayer setBorderWidth:2]; return self; } return nil; } - (void)drawInContext:(CGContextRef)context { if (_tileController) _tileController->drawTileMapContents(context, [self bounds]); } @end namespace WebCore { enum TileValidationPolicyFlag { PruneSecondaryTiles = 1 << 0, UnparentAllTiles = 1 << 1 }; static const int defaultTileWidth = 512; static const int defaultTileHeight = 512; PassOwnPtr<TileController> TileController::create(WebTiledBackingLayer* tileCacheLayer) { return adoptPtr(new TileController(tileCacheLayer)); } TileController::TileController(WebTiledBackingLayer* tileCacheLayer) : m_tileCacheLayer(tileCacheLayer) , m_tileContainerLayer(adoptNS([[CALayer alloc] init])) , m_tileSize(defaultTileWidth, defaultTileHeight) , m_tileRevalidationTimer(this, &TileController::tileRevalidationTimerFired) , m_cohortRemovalTimer(this, &TileController::cohortRemovalTimerFired) , m_scale(1) , m_deviceScaleFactor(1) , m_tileCoverage(CoverageForVisibleArea) , m_isInWindow(false) , m_scrollingPerformanceLoggingEnabled(false) , m_aggressivelyRetainsTiles(false) , m_unparentsOffscreenTiles(false) , m_acceleratesDrawing(false) , m_tilesAreOpaque(false) , m_clipsToExposedRect(false) , m_tileDebugBorderWidth(0) , m_indicatorMode(ThreadedScrollingIndication) { [CATransaction begin]; [CATransaction setDisableActions:YES]; [m_tileCacheLayer addSublayer:m_tileContainerLayer.get()]; #ifndef NDEBUG [m_tileContainerLayer.get() setName:@"TileController Container Layer"]; #endif [CATransaction commit]; } TileController::~TileController() { #if !PLATFORM(IOS) ASSERT(isMainThread()); #else ASSERT((isMainThread() || pthread_main_np()) && WebThreadIsLockedOrDisabled()); #endif for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setTileController:0]; } if (m_tiledScrollingIndicatorLayer) [m_tiledScrollingIndicatorLayer.get() setTileController:nil]; } void TileController::tileCacheLayerBoundsChanged() { if (m_tiles.isEmpty()) { // We must revalidate immediately instead of using a timer when there are // no tiles to avoid a flash when transitioning from one page to another. revalidateTiles(); return; } scheduleTileRevalidation(0); } void TileController::setNeedsDisplay() { for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setNeedsDisplay]; } } void TileController::setNeedsDisplayInRect(const IntRect& rect) { if (m_tiles.isEmpty()) return; FloatRect scaledRect(rect); scaledRect.scale(m_scale); IntRect repaintRectInTileCoords(enclosingIntRect(scaledRect)); // For small invalidations, lookup the covered tiles. if (repaintRectInTileCoords.height() < 2 * m_tileSize.height() && repaintRectInTileCoords.width() < 2 * m_tileSize.width()) { TileIndex topLeft; TileIndex bottomRight; getTileIndexRangeForRect(repaintRectInTileCoords, topLeft, bottomRight); for (int y = topLeft.y(); y <= bottomRight.y(); ++y) { for (int x = topLeft.x(); x <= bottomRight.x(); ++x) { TileIndex tileIndex(x, y); TileMap::iterator it = m_tiles.find(tileIndex); if (it != m_tiles.end()) setTileNeedsDisplayInRect(tileIndex, it->value, repaintRectInTileCoords, m_primaryTileCoverageRect); } } return; } for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) setTileNeedsDisplayInRect(it->key, it->value, repaintRectInTileCoords, m_primaryTileCoverageRect); } void TileController::setTileNeedsDisplayInRect(const TileIndex& tileIndex, TileInfo& tileInfo, const IntRect& repaintRectInTileCoords, const IntRect& coverageRectInTileCoords) { WebTileLayer* tileLayer = tileInfo.layer.get(); IntRect tileRect = rectForTileIndex(tileIndex); IntRect tileRepaintRect = tileRect; tileRepaintRect.intersect(repaintRectInTileCoords); if (tileRepaintRect.isEmpty()) return; tileRepaintRect.moveBy(-tileRect.location()); // We could test for intersection with the visible rect. This would reduce painting yet more, // but may make scrolling stale tiles into view more frequent. if (tileRect.intersects(coverageRectInTileCoords)) { [tileLayer setNeedsDisplayInRect:tileRepaintRect]; if (shouldShowRepaintCounters()) { CGRect bounds = [tileLayer bounds]; CGRect indicatorRect = CGRectMake(bounds.origin.x, bounds.origin.y, 52, 27); [tileLayer setNeedsDisplayInRect:indicatorRect]; } } else tileInfo.hasStaleContent = true; } void TileController::drawLayer(WebTileLayer *layer, CGContextRef context) { PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); if (!platformLayer) return; CGContextSaveGState(context); CGPoint layerOrigin = [layer frame].origin; CGContextTranslateCTM(context, -layerOrigin.x, -layerOrigin.y); CGContextScaleCTM(context, m_scale, m_scale); drawLayerContents(context, layer, platformLayer); CGContextRestoreGState(context); drawRepaintCounter(layer, context); } void TileController::setScale(CGFloat scale) { PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); float deviceScaleFactor = platformLayer->owner()->platformCALayerDeviceScaleFactor(); // The scale we get is the produce of the page scale factor and device scale factor. // Divide by the device scale factor so we'll get the page scale factor. scale /= deviceScaleFactor; if (m_scale == scale && m_deviceScaleFactor == deviceScaleFactor) return; #if PLATFORM(IOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 Vector<FloatRect> dirtyRects; m_deviceScaleFactor = deviceScaleFactor; m_scale = scale; [m_tileContainerLayer.get() setTransform:CATransform3DMakeScale(1 / m_scale, 1 / m_scale, 1)]; revalidateTiles(PruneSecondaryTiles, PruneSecondaryTiles); for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setContentsScale:deviceScaleFactor]; IntRect tileRect = rectForTileIndex(it->key); FloatRect scaledTileRect = tileRect; scaledTileRect.scale(1 / m_scale); dirtyRects.append(scaledTileRect); } platformLayer->owner()->platformCALayerDidCreateTiles(dirtyRects); #endif } void TileController::setAcceleratesDrawing(bool acceleratesDrawing) { #if PLATFORM(IOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 if (m_acceleratesDrawing == acceleratesDrawing) return; m_acceleratesDrawing = acceleratesDrawing; for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setAcceleratesDrawing:m_acceleratesDrawing]; } #else UNUSED_PARAM(acceleratesDrawing); #endif } void TileController::setTilesOpaque(bool opaque) { if (opaque == m_tilesAreOpaque) return; m_tilesAreOpaque = opaque; for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setOpaque:opaque]; } } void TileController::setVisibleRect(const FloatRect& visibleRect) { if (m_visibleRect == visibleRect) return; m_visibleRect = visibleRect; revalidateTiles(); } bool TileController::tilesWouldChangeForVisibleRect(const FloatRect& newVisibleRect) const { FloatRect visibleRect = newVisibleRect; if (m_clipsToExposedRect) visibleRect.intersect(m_exposedRect); if (visibleRect.isEmpty() || bounds().isEmpty()) return false; FloatRect currentTileCoverageRect = computeTileCoverageRect(m_visibleRect, newVisibleRect); FloatRect scaledRect(currentTileCoverageRect); scaledRect.scale(m_scale); IntRect currentCoverageRectInTileCoords(enclosingIntRect(scaledRect)); IntSize newTileSize = tileSizeForCoverageRect(currentTileCoverageRect); bool tileSizeChanged = newTileSize != m_tileSize; if (tileSizeChanged) return true; TileIndex topLeft; TileIndex bottomRight; getTileIndexRangeForRect(currentCoverageRectInTileCoords, topLeft, bottomRight); IntRect coverageRect = rectForTileIndex(topLeft); coverageRect.unite(rectForTileIndex(bottomRight)); return coverageRect != m_primaryTileCoverageRect; } void TileController::setExposedRect(const FloatRect& exposedRect) { if (m_exposedRect == exposedRect) return; m_exposedRect = exposedRect; revalidateTiles(); } void TileController::setClipsToExposedRect(bool clipsToExposedRect) { if (m_clipsToExposedRect == clipsToExposedRect) return; m_clipsToExposedRect = clipsToExposedRect; // Going from not clipping to clipping, we don't need to revalidate right away. if (clipsToExposedRect) revalidateTiles(); } void TileController::prepopulateRect(const FloatRect& rect) { ensureTilesForRect(rect); } void TileController::setIsInWindow(bool isInWindow) { if (m_isInWindow == isInWindow) return; m_isInWindow = isInWindow; if (m_isInWindow) revalidateTiles(); else { const double tileRevalidationTimeout = 4; scheduleTileRevalidation(tileRevalidationTimeout); } } void TileController::setTileCoverage(TileCoverage coverage) { if (coverage == m_tileCoverage) return; m_tileCoverage = coverage; scheduleTileRevalidation(0); } void TileController::forceRepaint() { setNeedsDisplay(); } void TileController::setTileDebugBorderWidth(float borderWidth) { if (m_tileDebugBorderWidth == borderWidth) return; m_tileDebugBorderWidth = borderWidth; for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setBorderWidth:m_tileDebugBorderWidth]; } } void TileController::setTileDebugBorderColor(CGColorRef borderColor) { if (m_tileDebugBorderColor == borderColor) return; m_tileDebugBorderColor = borderColor; for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; [tileInfo.layer.get() setBorderColor:m_tileDebugBorderColor.get()]; } } IntRect TileController::bounds() const { return IntRect(IntPoint(), IntSize([m_tileCacheLayer bounds].size)); } IntRect TileController::rectForTileIndex(const TileIndex& tileIndex) const { IntRect rect(tileIndex.x() * m_tileSize.width(), tileIndex.y() * m_tileSize.height(), m_tileSize.width(), m_tileSize.height()); IntRect scaledBounds(bounds()); scaledBounds.scale(m_scale); rect.intersect(scaledBounds); return rect; } void TileController::getTileIndexRangeForRect(const IntRect& rect, TileIndex& topLeft, TileIndex& bottomRight) const { IntRect clampedRect = bounds(); clampedRect.scale(m_scale); clampedRect.intersect(rect); topLeft.setX(max(clampedRect.x() / m_tileSize.width(), 0)); topLeft.setY(max(clampedRect.y() / m_tileSize.height(), 0)); int bottomXRatio = ceil((float)clampedRect.maxX() / m_tileSize.width()); bottomRight.setX(max(bottomXRatio - 1, 0)); int bottomYRatio = ceil((float)clampedRect.maxY() / m_tileSize.height()); bottomRight.setY(max(bottomYRatio - 1, 0)); } FloatRect TileController::computeTileCoverageRect(const FloatRect& previousVisibleRect, const FloatRect& currentVisibleRect) const { FloatRect visibleRect = currentVisibleRect; if (m_clipsToExposedRect) visibleRect.intersect(m_exposedRect); // If the page is not in a window (for example if it's in a background tab), we limit the tile coverage rect to the visible rect. // Furthermore, if the page can't have scrollbars (for example if its body element has overflow:hidden) it's very unlikely that the // page will ever be scrolled so we limit the tile coverage rect as well. if (!m_isInWindow || m_tileCoverage & CoverageForSlowScrolling) return visibleRect; bool largeVisibleRectChange = !previousVisibleRect.isEmpty() && !visibleRect.intersects(previousVisibleRect); // FIXME: look at how far the document can scroll in each dimension. float coverageHorizontalSize = visibleRect.width(); float coverageVerticalSize = visibleRect.height(); // Inflate the coverage rect so that it covers 2x of the visible width and 3x of the visible height. // These values were chosen because it's more common to have tall pages and to scroll vertically, // so we keep more tiles above and below the current area. if (m_tileCoverage & CoverageForHorizontalScrolling && !largeVisibleRectChange) coverageHorizontalSize *= 2; if (m_tileCoverage & CoverageForVerticalScrolling && !largeVisibleRectChange) coverageVerticalSize *= 3; // Don't extend coverage before 0 or after the end. FloatRect coverageBounds = bounds(); float coverageLeft = visibleRect.x() - (coverageHorizontalSize - visibleRect.width()) / 2; coverageLeft = min(coverageLeft, coverageBounds.maxX() - coverageHorizontalSize); coverageLeft = max(coverageLeft, coverageBounds.x()); float coverageTop = visibleRect.y() - (coverageVerticalSize - visibleRect.height()) / 2; coverageTop = min(coverageTop, coverageBounds.maxY() - coverageVerticalSize); coverageTop = max(coverageTop, coverageBounds.y()); return FloatRect(coverageLeft, coverageTop, coverageHorizontalSize, coverageVerticalSize); } IntSize TileController::tileSizeForCoverageRect(const FloatRect& coverageRect) const { if (m_tileCoverage & CoverageForSlowScrolling) { FloatSize tileSize = coverageRect.size(); tileSize.scale(m_scale); return expandedIntSize(tileSize); } return IntSize(defaultTileWidth, defaultTileHeight); } void TileController::scheduleTileRevalidation(double interval) { if (m_tileRevalidationTimer.isActive() && m_tileRevalidationTimer.nextFireInterval() < interval) return; m_tileRevalidationTimer.startOneShot(interval); } void TileController::tileRevalidationTimerFired(Timer<TileController>*) { TileValidationPolicyFlags foregroundValidationPolicy = m_aggressivelyRetainsTiles ? 0 : PruneSecondaryTiles; TileValidationPolicyFlags backgroundValidationPolicy = foregroundValidationPolicy | UnparentAllTiles; revalidateTiles(foregroundValidationPolicy, backgroundValidationPolicy); } unsigned TileController::blankPixelCount() const { WebTileLayerList tiles(m_tiles.size()); for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) tiles.append(it->value.layer); return blankPixelCountForTiles(tiles, m_visibleRect, IntPoint(0,0)); } unsigned TileController::blankPixelCountForTiles(const WebTileLayerList& tiles, const FloatRect& visibleRect, const IntPoint& tileTranslation) { Region paintedVisibleTiles; for (WebTileLayerList::const_iterator it = tiles.begin(), end = tiles.end(); it != end; ++it) { const WebTileLayer* tileLayer = it->get(); FloatRect visiblePart(CGRectOffset([tileLayer frame], tileTranslation.x(), tileTranslation.y())); visiblePart.intersect(visibleRect); if (!visiblePart.isEmpty()) paintedVisibleTiles.unite(enclosingIntRect(visiblePart)); } Region uncoveredRegion(enclosingIntRect(visibleRect)); uncoveredRegion.subtract(paintedVisibleTiles); return uncoveredRegion.totalArea(); } static inline void queueTileForRemoval(const TileController::TileIndex& tileIndex, const TileController::TileInfo& tileInfo, Vector<TileController::TileIndex>& tilesToRemove) { WebTileLayer* tileLayer = tileInfo.layer.get(); [tileLayer removeFromSuperlayer]; [tileLayer setTileController:0]; tilesToRemove.append(tileIndex); } void TileController::removeAllTiles() { Vector<TileIndex> tilesToRemove; for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) queueTileForRemoval(it->key, it->value, tilesToRemove); for (size_t i = 0; i < tilesToRemove.size(); ++i) { TileInfo tileInfo = m_tiles.take(tilesToRemove[i]); #if !PLATFORM(IOS) LayerPool::sharedPool()->addLayer(tileInfo.layer); #endif } } void TileController::removeAllSecondaryTiles() { Vector<TileIndex> tilesToRemove; for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; if (tileInfo.cohort == VisibleTileCohort) continue; queueTileForRemoval(it->key, it->value, tilesToRemove); } for (size_t i = 0; i < tilesToRemove.size(); ++i) { TileInfo tileInfo = m_tiles.take(tilesToRemove[i]); #if !PLATFORM(IOS) LayerPool::sharedPool()->addLayer(tileInfo.layer); #endif } } void TileController::removeTilesInCohort(TileCohort cohort) { ASSERT(cohort != VisibleTileCohort); Vector<TileIndex> tilesToRemove; for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; if (tileInfo.cohort != cohort) continue; queueTileForRemoval(it->key, it->value, tilesToRemove); } for (size_t i = 0; i < tilesToRemove.size(); ++i) { TileInfo tileInfo = m_tiles.take(tilesToRemove[i]); #if !PLATFORM(IOS) LayerPool::sharedPool()->addLayer(tileInfo.layer); #endif } } void TileController::revalidateTiles(TileValidationPolicyFlags foregroundValidationPolicy, TileValidationPolicyFlags backgroundValidationPolicy) { // If the underlying PlatformLayer has been destroyed, but the WebTiledBackingLayer hasn't // platformLayer will be null here. PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); if (!platformLayer) return; FloatRect visibleRect = m_visibleRect; IntRect bounds = this->bounds(); if (m_clipsToExposedRect) visibleRect.intersect(m_exposedRect); if (visibleRect.isEmpty() || bounds.isEmpty()) return; TileValidationPolicyFlags validationPolicy = m_isInWindow ? foregroundValidationPolicy : backgroundValidationPolicy; FloatRect tileCoverageRect = computeTileCoverageRect(m_visibleRectAtLastRevalidate, m_visibleRect); FloatRect scaledRect(tileCoverageRect); scaledRect.scale(m_scale); IntRect coverageRectInTileCoords(enclosingIntRect(scaledRect)); IntSize oldTileSize = m_tileSize; m_tileSize = tileSizeForCoverageRect(tileCoverageRect); bool tileSizeChanged = m_tileSize != oldTileSize; if (tileSizeChanged) { removeAllTiles(); m_cohortList.clear(); } else { TileCohort currCohort = nextTileCohort(); unsigned tilesInCohort = 0; // Move tiles newly outside the coverage rect into the cohort map. for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { TileInfo& tileInfo = it->value; TileIndex tileIndex = it->key; WebTileLayer* tileLayer = tileInfo.layer.get(); IntRect tileRect = rectForTileIndex(tileIndex); if (tileRect.intersects(coverageRectInTileCoords)) { tileInfo.cohort = VisibleTileCohort; if (tileInfo.hasStaleContent) { // FIXME: store a dirty region per layer? [tileLayer setNeedsDisplay]; tileInfo.hasStaleContent = false; } } else { // Add to the currentCohort if not already in one. if (tileInfo.cohort == VisibleTileCohort) { tileInfo.cohort = currCohort; ++tilesInCohort; if (m_unparentsOffscreenTiles) [tileInfo.layer.get() removeFromSuperlayer]; } } } if (tilesInCohort) startedNewCohort(currCohort); if (!m_aggressivelyRetainsTiles) scheduleCohortRemoval(); } TileIndex topLeft; TileIndex bottomRight; getTileIndexRangeForRect(coverageRectInTileCoords, topLeft, bottomRight); Vector<FloatRect> dirtyRects; // Ensure primary tile coverage tiles. m_primaryTileCoverageRect = IntRect(); for (int y = topLeft.y(); y <= bottomRight.y(); ++y) { for (int x = topLeft.x(); x <= bottomRight.x(); ++x) { TileIndex tileIndex(x, y); IntRect tileRect = rectForTileIndex(tileIndex); m_primaryTileCoverageRect.unite(tileRect); bool shouldChangeTileLayerFrame = false; TileInfo& tileInfo = m_tiles.add(tileIndex, TileInfo()).iterator->value; if (!tileInfo.layer) tileInfo.layer = createTileLayer(tileRect); else { // We already have a layer for this tile. Ensure that its size is correct. FloatSize tileLayerSize([tileInfo.layer.get() frame].size); shouldChangeTileLayerFrame = tileLayerSize != FloatSize(tileRect.size()); if (shouldChangeTileLayerFrame) [tileInfo.layer.get() setFrame:tileRect]; } bool shouldParentTileLayer = (!m_unparentsOffscreenTiles || m_isInWindow) && ![tileInfo.layer.get() superlayer]; if (shouldParentTileLayer) [m_tileContainerLayer.get() addSublayer:tileInfo.layer.get()]; if ((shouldParentTileLayer && [tileInfo.layer.get() needsDisplay]) || shouldChangeTileLayerFrame) { FloatRect scaledTileRect = tileRect; scaledTileRect.scale(1 / m_scale); dirtyRects.append(scaledTileRect); } } } if (validationPolicy & PruneSecondaryTiles) { removeAllSecondaryTiles(); m_cohortList.clear(); } if (m_unparentsOffscreenTiles && (validationPolicy & UnparentAllTiles)) { for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) [it->value.layer.get() removeFromSuperlayer]; } if (m_boundsAtLastRevalidate != bounds) { FloatRect scaledBounds(bounds); scaledBounds.scale(m_scale); IntRect boundsInTileCoords(enclosingIntRect(scaledBounds)); TileIndex topLeftForBounds; TileIndex bottomRightForBounds; getTileIndexRangeForRect(boundsInTileCoords, topLeftForBounds, bottomRightForBounds); Vector<TileIndex> tilesToRemove; for (TileMap::iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileIndex& index = it->key; if (index.y() < topLeftForBounds.y() || index.y() > bottomRightForBounds.y() || index.x() < topLeftForBounds.x() || index.x() > bottomRightForBounds.x()) queueTileForRemoval(index, it->value, tilesToRemove); } for (size_t i = 0, size = tilesToRemove.size(); i < size; ++i) { TileInfo tileInfo = m_tiles.take(tilesToRemove[i]); #if !PLATFORM(IOS) LayerPool::sharedPool()->addLayer(tileInfo.layer); #endif } } if (m_tiledScrollingIndicatorLayer) updateTileCoverageMap(); m_visibleRectAtLastRevalidate = visibleRect; m_boundsAtLastRevalidate = bounds; if (dirtyRects.isEmpty()) return; // This will ensure we flush compositing state and do layout in this run loop iteration. platformLayer->owner()->platformCALayerDidCreateTiles(dirtyRects); } TileController::TileCohort TileController::nextTileCohort() const { if (!m_cohortList.isEmpty()) return m_cohortList.last().cohort + 1; return 1; } void TileController::startedNewCohort(TileCohort cohort) { m_cohortList.append(TileCohortInfo(cohort, monotonicallyIncreasingTime())); } TileController::TileCohort TileController::newestTileCohort() const { return m_cohortList.isEmpty() ? 0 : m_cohortList.last().cohort; } TileController::TileCohort TileController::oldestTileCohort() const { return m_cohortList.isEmpty() ? 0 : m_cohortList.first().cohort; } void TileController::scheduleCohortRemoval() { const double cohortRemovalTimerSeconds = 1; // Start the timer, or reschedule the timer from now if it's already active. if (!m_cohortRemovalTimer.isActive()) m_cohortRemovalTimer.startRepeating(cohortRemovalTimerSeconds); } void TileController::cohortRemovalTimerFired(Timer<TileController>*) { if (m_cohortList.isEmpty()) { m_cohortRemovalTimer.stop(); return; } double cohortLifeTimeSeconds = 2; double timeThreshold = monotonicallyIncreasingTime() - cohortLifeTimeSeconds; while (!m_cohortList.isEmpty() && m_cohortList.first().creationTime < timeThreshold) { TileCohortInfo firstCohort = m_cohortList.takeFirst(); removeTilesInCohort(firstCohort.cohort); } if (m_tiledScrollingIndicatorLayer) updateTileCoverageMap(); } void TileController::ensureTilesForRect(const FloatRect& rect) { if (m_unparentsOffscreenTiles && !m_isInWindow) return; PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); if (!platformLayer) return; FloatRect scaledRect(rect); scaledRect.scale(m_scale); IntRect rectInTileCoords(enclosingIntRect(scaledRect)); if (m_primaryTileCoverageRect.contains(rectInTileCoords)) return; TileIndex topLeft; TileIndex bottomRight; getTileIndexRangeForRect(rectInTileCoords, topLeft, bottomRight); Vector<FloatRect> dirtyRects; TileCohort currCohort = nextTileCohort(); unsigned tilesInCohort = 0; for (int y = topLeft.y(); y <= bottomRight.y(); ++y) { for (int x = topLeft.x(); x <= bottomRight.x(); ++x) { TileIndex tileIndex(x, y); IntRect tileRect = rectForTileIndex(tileIndex); TileInfo& tileInfo = m_tiles.add(tileIndex, TileInfo()).iterator->value; bool shouldChangeTileLayerFrame = false; if (!tileInfo.layer) tileInfo.layer = createTileLayer(tileRect); else { // We already have a layer for this tile. Ensure that its size is correct. CGSize tileLayerSize = [tileInfo.layer.get() frame].size; shouldChangeTileLayerFrame = tileLayerSize.width < tileRect.width() || tileLayerSize.height < tileRect.height(); if (shouldChangeTileLayerFrame) [tileInfo.layer.get() setFrame:tileRect]; } if (!tileRect.intersects(m_primaryTileCoverageRect)) { tileInfo.cohort = currCohort; ++tilesInCohort; } bool shouldParentTileLayer = ![tileInfo.layer.get() superlayer]; if (shouldParentTileLayer) [m_tileContainerLayer.get() addSublayer:tileInfo.layer.get()]; if ((shouldParentTileLayer && [tileInfo.layer.get() needsDisplay]) || shouldChangeTileLayerFrame) { FloatRect scaledTileRect = tileRect; scaledTileRect.scale(1 / m_scale); dirtyRects.append(scaledTileRect); } } } if (tilesInCohort) startedNewCohort(currCohort); if (m_tiledScrollingIndicatorLayer) updateTileCoverageMap(); // This will ensure we flush compositing state and do layout in this run loop iteration. if (!dirtyRects.isEmpty()) platformLayer->owner()->platformCALayerDidCreateTiles(dirtyRects); } void TileController::updateTileCoverageMap() { FloatRect containerBounds = bounds(); FloatRect visibleRect = this->visibleRect(); if (m_clipsToExposedRect) visibleRect.intersect(m_exposedRect); visibleRect.contract(4, 4); // Layer is positioned 2px from top and left edges. float widthScale = 1; float scale = 1; if (!containerBounds.isEmpty()) { widthScale = std::min<float>(visibleRect.width() / containerBounds.width(), 0.1); scale = std::min(widthScale, visibleRect.height() / containerBounds.height()); } float indicatorScale = scale * m_scale; FloatRect mapBounds = containerBounds; mapBounds.scale(indicatorScale, indicatorScale); BEGIN_BLOCK_OBJC_EXCEPTIONS if (m_clipsToExposedRect) [m_tiledScrollingIndicatorLayer.get() setPosition:m_exposedRect.location() + FloatPoint(2, 2)]; else [m_tiledScrollingIndicatorLayer.get() setPosition:CGPointMake(2, 2)]; [m_tiledScrollingIndicatorLayer.get() setBounds:mapBounds]; [m_tiledScrollingIndicatorLayer.get() setNeedsDisplay]; visibleRect.scale(indicatorScale, indicatorScale); visibleRect.expand(2, 2); [[m_tiledScrollingIndicatorLayer.get() visibleRectFrameLayer] setFrame:visibleRect]; Color backgroundColor; switch (m_indicatorMode) { case MainThreadScrollingBecauseOfStyleIndication: backgroundColor = Color(255, 0, 0); break; case MainThreadScrollingBecauseOfEventHandlersIndication: backgroundColor = Color(255, 255, 0); break; case ThreadedScrollingIndication: backgroundColor = Color(0, 200, 0); break; } [[m_tiledScrollingIndicatorLayer.get() visibleRectFrameLayer] setBorderColor:cachedCGColor(backgroundColor, ColorSpaceDeviceRGB)]; END_BLOCK_OBJC_EXCEPTIONS } IntRect TileController::tileGridExtent() const { TileIndex topLeft; TileIndex bottomRight; getTileIndexRangeForRect(m_primaryTileCoverageRect, topLeft, bottomRight); // Return index of top, left tile and the number of tiles across and down. return IntRect(topLeft.x(), topLeft.y(), bottomRight.x() - topLeft.x() + 1, bottomRight.y() - topLeft.y() + 1); } double TileController::retainedTileBackingStoreMemory() const { double totalBytes = 0; for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; if ([tileInfo.layer.get() superlayer]) { CGRect bounds = [tileInfo.layer.get() bounds]; double contentsScale = [tileInfo.layer.get() contentsScale]; totalBytes += 4 * bounds.size.width * contentsScale * bounds.size.height * contentsScale; } } return totalBytes; } // Return the rect in layer coords, not tile coords. IntRect TileController::tileCoverageRect() const { IntRect coverageRectInLayerCoords(m_primaryTileCoverageRect); coverageRectInLayerCoords.scale(1 / m_scale); return coverageRectInLayerCoords; } CALayer *TileController::tiledScrollingIndicatorLayer() { if (!m_tiledScrollingIndicatorLayer) { m_tiledScrollingIndicatorLayer = [WebTiledScrollingIndicatorLayer layer]; [m_tiledScrollingIndicatorLayer.get() setTileController:this]; [m_tiledScrollingIndicatorLayer.get() setOpacity:0.75]; [m_tiledScrollingIndicatorLayer.get() setAnchorPoint:CGPointZero]; [m_tiledScrollingIndicatorLayer.get() setBorderColor:cachedCGColor(Color::black, ColorSpaceDeviceRGB)]; [m_tiledScrollingIndicatorLayer.get() setBorderWidth:1]; [m_tiledScrollingIndicatorLayer.get() setPosition:CGPointMake(2, 2)]; updateTileCoverageMap(); } return m_tiledScrollingIndicatorLayer.get(); } void TileController::setScrollingModeIndication(ScrollingModeIndication scrollingMode) { if (scrollingMode == m_indicatorMode) return; m_indicatorMode = scrollingMode; if (m_tiledScrollingIndicatorLayer) updateTileCoverageMap(); } WebTileLayer* TileController::tileLayerAtIndex(const TileIndex& index) const { return m_tiles.get(index).layer.get(); } RetainPtr<WebTileLayer> TileController::createTileLayer(const IntRect& tileRect) { #if PLATFORM(IOS) RetainPtr<WebTileLayer> layer; #else RetainPtr<WebTileLayer> layer = LayerPool::sharedPool()->takeLayerWithSize(tileRect.size()); #endif if (layer) [layer resetPaintCount]; else layer = adoptNS([[WebTileLayer alloc] init]); [layer.get() setAnchorPoint:CGPointZero]; [layer.get() setFrame:tileRect]; [layer.get() setTileController:this]; [layer.get() setBorderColor:m_tileDebugBorderColor.get()]; [layer.get() setBorderWidth:m_tileDebugBorderWidth]; [layer.get() setEdgeAntialiasingMask:0]; [layer.get() setOpaque:m_tilesAreOpaque]; #ifndef NDEBUG [layer.get() setName:@"Tile"]; #endif #if PLATFORM(IOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 [layer.get() setContentsScale:m_deviceScaleFactor]; [layer.get() setAcceleratesDrawing:m_acceleratesDrawing]; #endif [layer setNeedsDisplay]; return layer; } bool TileController::shouldShowRepaintCounters() const { PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); if (!platformLayer) return false; WebCore::PlatformCALayerClient* layerContents = platformLayer->owner(); ASSERT(layerContents); if (!layerContents) return false; return layerContents->platformCALayerShowRepaintCounter(0); } void TileController::drawRepaintCounter(WebTileLayer *layer, CGContextRef context) { unsigned paintCount = [layer incrementPaintCount]; if (!shouldShowRepaintCounters()) return; // FIXME: Some of this code could be shared with WebLayer. char text[16]; // that's a lot of repaints snprintf(text, sizeof(text), "%d", paintCount); CGRect indicatorBox = [layer bounds]; indicatorBox.size.width = 12 + 10 * strlen(text); indicatorBox.size.height = 27; CGContextSaveGState(context); CGContextSetAlpha(context, 0.5f); CGContextBeginTransparencyLayerWithRect(context, indicatorBox, 0); CGContextSetFillColorWithColor(context, m_tileDebugBorderColor.get()); CGContextFillRect(context, indicatorBox); PlatformCALayer* platformLayer = PlatformCALayer::platformCALayer(m_tileCacheLayer); if (platformLayer->acceleratesDrawing()) CGContextSetRGBFillColor(context, 1, 0, 0, 1); else CGContextSetRGBFillColor(context, 1, 1, 1, 1); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1)); CGContextSelectFont(context, "Helvetica", 22, kCGEncodingMacRoman); CGContextShowTextAtPoint(context, indicatorBox.origin.x + 5, indicatorBox.origin.y + 22, text, strlen(text)); #pragma clang diagnostic pop CGContextEndTransparencyLayer(context); CGContextRestoreGState(context); } void TileController::drawTileMapContents(CGContextRef context, CGRect layerBounds) { CGContextSetRGBFillColor(context, 0.3, 0.3, 0.3, 1); CGContextFillRect(context, layerBounds); CGFloat scaleFactor = layerBounds.size.width / bounds().width(); CGFloat contextScale = scaleFactor / scale(); CGContextScaleCTM(context, contextScale, contextScale); for (TileMap::const_iterator it = m_tiles.begin(), end = m_tiles.end(); it != end; ++it) { const TileInfo& tileInfo = it->value; WebTileLayer* tileLayer = tileInfo.layer.get(); CGFloat red = 1; CGFloat green = 1; CGFloat blue = 1; if (tileInfo.hasStaleContent) { red = 0.25; green = 0.125; blue = 0; } TileCohort newestCohort = newestTileCohort(); TileCohort oldestCohort = oldestTileCohort(); if (!m_aggressivelyRetainsTiles && tileInfo.cohort != VisibleTileCohort && newestCohort > oldestCohort) { float cohortProportion = static_cast<float>((newestCohort - tileInfo.cohort)) / (newestCohort - oldestCohort); CGContextSetRGBFillColor(context, red, green, blue, 1 - cohortProportion); } else CGContextSetRGBFillColor(context, red, green, blue, 1); if ([tileLayer superlayer]) { CGContextSetLineWidth(context, 0.5 / contextScale); CGContextSetRGBStrokeColor(context, 0, 0, 0, 1); } else { CGContextSetLineWidth(context, 1 / contextScale); CGContextSetRGBStrokeColor(context, 0.2, 0.1, 0.9, 1); } CGRect frame = [tileLayer frame]; CGContextFillRect(context, frame); CGContextStrokeRect(context, frame); } } } // namespace WebCore