diff --git a/CHANGES.md b/CHANGES.md index 863df63890e3..2b2df97f8f65 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,12 @@ Change Log * Added optional `width` and `height` to `Scene.drillPick` for specifying a search area. ##### Fixes :wrench: +* Several performance improvements and fixes to the 3D Tiles traversal code. [#6390](https://github.com/AnalyticalGraphicsInc/cesium/pull/6390) + * Improved load performance when `skipLevelOfDetail` is false. + * Fixed a bug that caused some skipped tiles to load when `skipLevelOfDetail` is true. + * Fixed pick statistics in the 3D Tiles Inspector. + * Fixed drawing of debug labels for external tilesets. + * Fixed drawing of debug outlines for empty tiles. * The Geocoder widget now takes terrain altitude into account when calculating its final destination. * The Viewer widget now takes terrain altitude into account when zooming or flying to imagery layers. * Fixed `getPickRay` in 2D. [#2480](https://github.com/AnalyticalGraphicsInc/cesium/issues/2480) diff --git a/Source/Core/ManagedArray.js b/Source/Core/ManagedArray.js index fed116228452..8c63178ed689 100644 --- a/Source/Core/ManagedArray.js +++ b/Source/Core/ManagedArray.js @@ -74,9 +74,9 @@ define([ * Sets the element at an index. Resizes the array if index is greater than the length of the array. * * @param {Number} index The index to set. - * @param {*} value The value to set at index. + * @param {*} element The element to set at index. */ - ManagedArray.prototype.set = function(index, value) { + ManagedArray.prototype.set = function(index, element) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number('index', index); //>>includeEnd('debug'); @@ -84,11 +84,22 @@ define([ if (index >= this.length) { this.length = index + 1; } - this._array[index] = value; + this._array[index] = element; + }; + + /** + * Returns the last element in the array without modifying the array. + * + * @returns {*} The last element in the array. + */ + ManagedArray.prototype.peek = function() { + return this._array[this._length - 1]; }; /** * Push an element into the array. + * + * @param {*} element The element to push. */ ManagedArray.prototype.push = function(element) { var index = this.length++; diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 8d0221741ee4..f3e291eaaa40 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -482,8 +482,7 @@ define([ // If any commands were pushed, add derived commands var commandEnd = frameState.commandList.length; if ((commandStart < commandEnd) && (frameState.passes.render || frameState.passes.pick) && !defined(tileset.classificationType)) { - var finalResolution = this._tile._finalResolution; - this._batchTable.addDerivedCommands(frameState, commandStart, finalResolution); + this._batchTable.addDerivedCommands(frameState, commandStart); } }; diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index c01c67cc1f68..95aafb56ae90 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -2,6 +2,7 @@ define([ '../Core/BoundingSphere', '../Core/Cartesian3', '../Core/Color', + '../Core/ColorGeometryInstanceAttribute', '../Core/CullingVolume', '../Core/defaultValue', '../Core/defined', @@ -24,7 +25,6 @@ define([ '../Core/Resource', '../Core/RuntimeError', '../ThirdParty/when', - './Cesium3DTileChildrenVisibility', './Cesium3DTileContentFactory', './Cesium3DTileContentState', './Cesium3DTileOptimizationHint', @@ -38,6 +38,7 @@ define([ BoundingSphere, Cartesian3, Color, + ColorGeometryInstanceAttribute, CullingVolume, defaultValue, defined, @@ -60,7 +61,6 @@ define([ Resource, RuntimeError, when, - Cesium3DTileChildrenVisibility, Cesium3DTileContentFactory, Cesium3DTileContentState, Cesium3DTileOptimizationHint, @@ -235,19 +235,6 @@ define([ */ this.hasEmptyContent = hasEmptyContent; - /** - * When true, the tile's content is renderable. - *

- * This is false until the tile's content is loaded. - *

- * - * @type {Boolean} - * @readonly - * - * @private - */ - this.hasRenderableContent = false; - /** * When true, the tile's content points to an external tileset. *

@@ -262,14 +249,16 @@ define([ this.hasTilesetContent = false; /** - * The corresponding node in the cache replacement list. + * The node in the tileset's LRU cache, used to determine when to unload a tile's content. + * + * See {@link Cesium3DTilesetCache} * * @type {DoublyLinkedListNode} * @readonly * * @private */ - this.replacementNode = undefined; + this.cacheNode = undefined; var expire = header.expire; var expireDuration; @@ -295,15 +284,6 @@ define([ */ this.expireDate = expireDate; - /** - * Marks if the tile is selected this frame. - * - * @type {Boolean} - * - * @private - */ - this.selected = false; - /** * The time when a style was last applied to this tile. * @@ -334,22 +314,27 @@ define([ // Members that are updated every frame for tree traversal and rendering optimizations: this._distanceToCamera = 0; - this._visibilityPlaneMask = 0; - this._childrenVisibility = Cesium3DTileChildrenVisibility.VISIBLE; - this._lastSelectedFrameNumber = -1; + this._centerZDepth = 0; this._screenSpaceError = 0; - this._screenSpaceErrorComputedFrame = -1; + this._visibilityPlaneMask = 0; + this._visible = false; + this._inRequestVolume = false; + this._finalResolution = true; this._depth = 0; - this._centerZDepth = 0; this._stackLength = 0; - this._selectedFrame = -1; this._selectionDepth = 0; - this._lastSelectionDepth = undefined; - this._requestedFrame = undefined; - this._lastVisitedFrame = undefined; + + this._updatedVisibilityFrame = 0; + this._touchedFrame = 0; + this._visitedFrame = 0; + this._selectedFrame = 0; + this._requestedFrame = 0; this._ancestorWithContent = undefined; - this._ancestorWithLoadedContent = undefined; + this._ancestorWithContentAvailable = undefined; + this._refines = false; + this._shouldSelect = false; + this._priority = 0.0; this._isClipped = true; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function @@ -467,13 +452,13 @@ define([ */ contentAvailable : { get : function() { - return this.contentReady || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); + return (this.contentReady && !this.hasEmptyContent && !this.hasTilesetContent) || (defined(this._expiredContent) && !this.contentFailed); } }, /** - * Determines if the tile is ready to render. true if the tile - * is ready to render; otherwise, false. + * Determines if the tile's content is ready. This is automatically true for + * tile's with empty content. * * @memberof Cesium3DTile.prototype * @@ -522,6 +507,23 @@ define([ } }, + /** + * Determines if the tile's content failed to load. true if the tile's + * content failed to load; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentFailed : { + get : function() { + return this._contentState === Cesium3DTileContentState.FAILED; + } + }, + /** * Gets the promise that will be resolved when the tile's content is ready to process. * This happens after the content is downloaded but before the content is ready @@ -618,7 +620,7 @@ define([ function createPriorityFunction(tile) { return function() { - return tile._distanceToCamera; + return tile._priority; }; } @@ -690,7 +692,6 @@ define([ if (defined(contentFactory)) { content = contentFactory(tileset, that, that._contentResource, arrayBuffer, 0); - that.hasRenderableContent = true; } else { // The content may be json instead content = Cesium3DTileContentFactory.json(tileset, that, that._contentResource, arrayBuffer, 0); @@ -710,7 +711,7 @@ define([ updateExpireDate(that); // Refresh style for expired content - that.lastStyleTime = 0; + that._selectedFrame = 0; that._contentState = Cesium3DTileContentState.READY; that._contentReadyPromise.resolve(content); @@ -735,7 +736,7 @@ define([ * @private */ Cesium3DTile.prototype.unloadContent = function() { - if (!this.hasRenderableContent) { + if (this.hasEmptyContent || this.hasTilesetContent) { return; } @@ -744,8 +745,6 @@ define([ this._contentReadyToProcessPromise = undefined; this._contentReadyPromise = undefined; - this.replacementNode = undefined; - this.lastStyleTime = 0; this.clippingPlanesDirty = (this._clippingPlanesState === 0); this._clippingPlanesState = 0; @@ -859,7 +858,7 @@ define([ * Computes the distance from the center of the tile's bounding volume to the camera. * * @param {FrameState} frameState The frame state. - * @returns {Number} The distance, in meters, or zero if the camera is inside the bounding volume. + * @returns {Number} The distance, in meters. * * @private */ @@ -1032,14 +1031,24 @@ define([ function applyDebugSettings(tile, tileset, frameState) { var hasContentBoundingVolume = defined(tile._header.content) && defined(tile._header.content.boundingVolume); + var empty = tile.hasEmptyContent || tile.hasTilesetContent; var showVolume = tileset.debugShowBoundingVolume || (tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume); if (showVolume) { + var color; + if (!tile._finalResolution) { + color = Color.YELLOW; + } else if (empty) { + color = Color.DARKGRAY; + } else { + color = Color.WHITE; + } if (!defined(tile._debugBoundingVolume)) { - var color = tile._finalResolution ? (hasContentBoundingVolume ? Color.WHITE : Color.RED) : Color.YELLOW; tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); } tile._debugBoundingVolume.update(frameState); + var attributes = tile._debugBoundingVolume.getGeometryInstanceAttributes('outline'); + attributes.color = ColorGeometryInstanceAttribute.toValue(color, attributes.color); } else if (!showVolume && defined(tile._debugBoundingVolume)) { tile._debugBoundingVolume = tile._debugBoundingVolume.destroy(); } diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index ab95ba26d90d..dff3cb9a4d96 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1241,10 +1241,11 @@ define([ OPAQUE_AND_TRANSLUCENT : 2 }; - Cesium3DTileBatchTable.prototype.addDerivedCommands = function(frameState, commandStart, finalResolution) { + Cesium3DTileBatchTable.prototype.addDerivedCommands = function(frameState, commandStart) { var commandList = frameState.commandList; var commandEnd = commandList.length; var tile = this._content._tile; + var finalResolution = tile._finalResolution; var tileset = tile._tileset; var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer; var styleCommandsNeeded = getStyleCommandsNeeded(this); @@ -1276,9 +1277,8 @@ define([ } tileset._backfaceCommands.push(derivedCommands.zback); } - if (!defined(derivedCommands.stencil) || tile._selectionDepth !== tile._lastSelectionDepth) { + if (!defined(derivedCommands.stencil) || tile._selectionDepth !== getLastSelectionDepth(derivedCommands.stencil)) { derivedCommands.stencil = deriveStencilCommand(derivedCommands.originalCommand, tile._selectionDepth); - tile._lastSelectionDepth = tile._selectionDepth; } updateDerivedCommand(derivedCommands.stencil, command); } @@ -1423,6 +1423,10 @@ define([ return derivedCommand; } + function getLastSelectionDepth(stencilCommand) { + return stencilCommand.renderState.stencilTest.reference >>> 4; + } + function getTranslucentRenderState(renderState) { var rs = clone(renderState, true); rs.cull.enabled = false; diff --git a/Source/Scene/Cesium3DTileChildrenVisibility.js b/Source/Scene/Cesium3DTileChildrenVisibility.js deleted file mode 100644 index ddfd279b147d..000000000000 --- a/Source/Scene/Cesium3DTileChildrenVisibility.js +++ /dev/null @@ -1,19 +0,0 @@ -define([ - '../Core/freezeObject' - ], function( - freezeObject) { - 'use strict'; - - /** - * @private - */ - var Cesium3DTileChildrenVisibility = { - NONE : 0, // No children visible - VISIBLE : 1, // At least one child visible - IN_REQUEST_VOLUME : 2, // At least one child in viewer request volume - VISIBLE_IN_REQUEST_VOLUME : 4, // At least one child both visible and in viewer request volume - VISIBLE_NOT_IN_REQUEST_VOLUME : 8 // At least one child visible but not in viewer request volume - }; - - return freezeObject(Cesium3DTileChildrenVisibility); -}); diff --git a/Source/Scene/Cesium3DTileStyleEngine.js b/Source/Scene/Cesium3DTileStyleEngine.js index 2a645ae55fda..36d4c3fb0ab6 100644 --- a/Source/Scene/Cesium3DTileStyleEngine.js +++ b/Source/Scene/Cesium3DTileStyleEngine.js @@ -65,7 +65,7 @@ define([ var length = tiles.length; for (var i = 0; i < length; ++i) { var tile = tiles[i]; - if (tile.selected && (tile.lastStyleTime !== lastStyleTime)) { + if (tile.lastStyleTime !== lastStyleTime) { // Apply the style to this tile if it wasn't already applied because: // 1) the user assigned a new style to the tileset // 2) this tile is now visible, but it wasn't visible when the style was first assigned diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 7f7391569bb0..00e921139599 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -27,6 +27,8 @@ define([ './Cesium3DTileColorBlendMode', './Cesium3DTileContentState', './Cesium3DTileOptimizations', + './Cesium3DTileRefine', + './Cesium3DTilesetCache', './Cesium3DTilesetStatistics', './Cesium3DTilesetTraversal', './Cesium3DTileStyleEngine', @@ -68,6 +70,8 @@ define([ Cesium3DTileColorBlendMode, Cesium3DTileContentState, Cesium3DTileOptimizations, + Cesium3DTileRefine, + Cesium3DTilesetCache, Cesium3DTilesetStatistics, Cesium3DTilesetTraversal, Cesium3DTileStyleEngine, @@ -169,31 +173,20 @@ define([ this._geometricError = undefined; // Geometric error when the tree is not rendered at all this._extensionsUsed = undefined; this._gltfUpAxis = undefined; + this._cache = new Cesium3DTilesetCache(); this._processingQueue = []; this._selectedTiles = []; + this._emptyTiles = []; this._requestedTiles = []; - this._desiredTiles = new ManagedArray(); this._selectedTilesToStyle = []; this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; - var replacementList = new DoublyLinkedList(); - - // [head, sentinel) -> tiles that weren't selected this frame and may be replaced - // (sentinel, tail] -> tiles that were selected this frame - this._replacementList = replacementList; // Tiles with content loaded. For cache management. - this._replacementSentinel = replacementList.add(); - this._trimTiles = false; - this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); + this._allTilesAdditive = true; this._hasMixedContent = false; - this._baseTraversal = new Cesium3DTilesetTraversal.BaseTraversal(); - this._skipTraversal = new Cesium3DTilesetTraversal.SkipTraversal({ - selectionHeuristic : selectionHeuristic - }); - this._backfaceCommands = new ManagedArray(); this._maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); @@ -593,7 +586,7 @@ define([ * This property is for debugging only; it is not optimized for production use. *

* When true, renders the bounding volume for each visible tile. The bounding volume is - * white if the tile has a content bounding volume; otherwise, it is red. Tiles that don't meet the + * white if the tile has a content bounding volume or is empty; otherwise, it is red. Tiles that don't meet the * screen space error and are still refining to their descendants are yellow. *

* @@ -700,7 +693,6 @@ define([ that._url = resource.url; that._basePath = basePath; - // We don't know the distance of the tileset until tileset JSON file is loaded, so use the default distance for now return Cesium3DTileset.loadJson(resource); }) .then(function(tilesetJson) { @@ -1264,7 +1256,7 @@ define([ // Append the tileset version to the resource if (!defined(resource.queryParameters.v)) { var versionQuery = { - v: defaultValue(asset.tilesetVersion, '0.0') + v : defaultValue(asset.tilesetVersion, '0.0') }; this._basePath += '?v=' + versionQuery.v; resource.setQueryParameters(versionQuery); @@ -1281,35 +1273,27 @@ define([ rootTile._depth = parentTile._depth + 1; } - ++statistics.numberOfTilesTotal; - var stack = []; - stack.push({ - header : tilesetJson.root, - tile3D : rootTile - }); + stack.push(rootTile); while (stack.length > 0) { var tile = stack.pop(); - var tile3D = tile.tile3D; - var children = tile.header.children; + ++statistics.numberOfTilesTotal; + this._allTilesAdditive = this._allTilesAdditive && (tile.refine === Cesium3DTileRefine.ADD); + var children = tile._header.children; if (defined(children)) { var length = children.length; for (var i = 0; i < length; ++i) { var childHeader = children[i]; - var childTile = new Cesium3DTile(this, resource, childHeader, tile3D); - tile3D.children.push(childTile); - childTile._depth = tile3D._depth + 1; - ++statistics.numberOfTilesTotal; - stack.push({ - header : childHeader, - tile3D : childTile - }); + var childTile = new Cesium3DTile(this, resource, childHeader, tile); + tile.children.push(childTile); + childTile._depth = tile._depth + 1; + stack.push(childTile); } } if (this._cullWithChildrenBounds) { - Cesium3DTileOptimizations.checkChildrenWithinParent(tile3D); + Cesium3DTileOptimizations.checkChildrenWithinParent(tile); } } @@ -1395,15 +1379,6 @@ define([ tileset._dynamicScreenSpaceErrorComputedDensity = density; } - function selectionHeuristic(tileset, ancestor, tile) { - var skipLevels = tileset._skipLevelOfDetail ? tileset.skipLevels : 0; - var skipScreenSpaceErrorFactor = tileset._skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; - - return (ancestor !== tile && !tile.hasEmptyContent && !tileset.immediatelyLoadDesiredLevelOfDetail) && - (tile._screenSpaceError < ancestor._screenSpaceError / skipScreenSpaceErrorFactor) && - (tile._depth > ancestor._depth + skipLevels); - } - /////////////////////////////////////////////////////////////////////////// function requestContent(tileset, tile) { @@ -1421,11 +1396,11 @@ define([ } if (expired) { - if (tile.hasRenderableContent) { + if (tile.hasTilesetContent) { + destroySubtree(tileset, tile); + } else { statistics.decrementLoadCounts(tile.content); --tileset._statistics.numberOfTilesWithContentReady; - } else if (tile.hasTilesetContent) { - destroySubtree(tileset, tile); } } @@ -1435,12 +1410,16 @@ define([ tile.contentReadyPromise.then(handleTileSuccess(tileset, tile)).otherwise(handleTileFailure(tileset, tile)); } - function requestTiles(tileset, outOfCore) { - if (!outOfCore) { - return; - } + function sortRequestByPriority(a, b) { + return a._priority - b._priority; + } + + function requestTiles(tileset) { + // Sort requests by priority before making any requests. + // This makes it less likely that requests will be cancelled after being issued. var requestedTiles = tileset._requestedTiles; var length = requestedTiles.length; + requestedTiles.sort(sortRequestByPriority); for (var i = 0; i < length; ++i) { requestContent(tileset, requestedTiles[i]); } @@ -1483,16 +1462,14 @@ define([ return function() { --tileset._statistics.numberOfTilesProcessing; - if (tile.hasRenderableContent) { + if (!tile.hasTilesetContent) { // RESEARCH_IDEA: ability to unload tiles (without content) for an // external tileset when all the tiles are unloaded. tileset._statistics.incrementLoadCounts(tile.content); ++tileset._statistics.numberOfTilesWithContentReady; - // Add to the tile cache. Previously expired tiles are already in the cache. - if (!defined(tile.replacementNode)) { - tile.replacementNode = tileset._replacementList.add(tile); - } + // Add to the tile cache. Previously expired tiles are already in the cache and won't get re-added. + tileset._cache.add(tile); } tileset.tileLoad.raiseEvent(tile); @@ -1615,8 +1592,12 @@ define([ } function updateTileDebugLabels(tileset, frameState) { + var i; + var tile; var selectedTiles = tileset._selectedTiles; - var length = selectedTiles.length; + var selectedLength = selectedTiles.length; + var emptyTiles = tileset._emptyTiles; + var emptyLength = emptyTiles.length; tileset._tileDebugLabels.removeAll(); if (tileset.debugPickedTileLabelOnly) { @@ -1626,10 +1607,16 @@ define([ label.pixelOffset = new Cartesian2(15, -15); // Offset to avoid picking the label. } } else { - for (var i = 0; i < length; ++i) { - var tile = selectedTiles[i]; + for (i = 0; i < selectedLength; ++i) { + tile = selectedTiles[i]; addTileDebugLabel(tile, tileset, computeTileLabelPosition(tile)); } + for (i = 0; i < emptyLength; ++i) { + tile = emptyTiles[i]; + if (tile.hasTilesetContent) { + addTileDebugLabel(tile, tileset, computeTileLabelPosition(tile)); + } + } } tileset._tileDebugLabels.update(frameState); } @@ -1646,11 +1633,14 @@ define([ var commandList = frameState.commandList; var numberOfInitialCommands = commandList.length; var selectedTiles = tileset._selectedTiles; - var length = selectedTiles.length; + var selectedLength = selectedTiles.length; + var emptyTiles = tileset._emptyTiles; + var emptyLength = emptyTiles.length; var tileVisible = tileset.tileVisible; var i; + var tile; - var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; + var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && selectedLength > 0; tileset._backfaceCommands.length = 0; @@ -1659,18 +1649,20 @@ define([ } var lengthBeforeUpdate = commandList.length; - for (i = 0; i < length; ++i) { - var tile = selectedTiles[i]; - // tiles may get unloaded and destroyed between selection and update - if (tile.selected) { - // Raise the tileVisible event before update in case the tileVisible event - // handler makes changes that update needs to apply to WebGL resources - tileVisible.raiseEvent(tile); - tile.update(tileset, frameState); - statistics.incrementSelectionCounts(tile.content); - ++statistics.selected; - } + for (i = 0; i < selectedLength; ++i) { + tile = selectedTiles[i]; + // Raise the tileVisible event before update in case the tileVisible event + // handler makes changes that update needs to apply to WebGL resources + tileVisible.raiseEvent(tile); + tile.update(tileset, frameState); + statistics.incrementSelectionCounts(tile.content); + ++statistics.selected; + } + for (i = 0; i < emptyLength; ++i) { + tile = emptyTiles[i]; + tile.update(tileset, frameState); } + var lengthAfterUpdate = commandList.length; var addedCommandsLength = lengthAfterUpdate - lengthBeforeUpdate; @@ -1752,52 +1744,27 @@ define([ stack.push(children[i]); } if (tile !== root) { - unloadTileFromCache(tileset, tile); - tile.destroy(); + destroyTile(tileset, tile); --statistics.numberOfTilesTotal; } } root.children = []; } - function unloadTileFromCache(tileset, tile) { - var node = tile.replacementNode; - if (!defined(node)) { - return; - } - - var statistics = tileset._statistics; - var replacementList = tileset._replacementList; - var tileUnload = tileset.tileUnload; + function unloadTile(tileset, tile) { + tileset.tileUnload.raiseEvent(tile); + tileset._statistics.decrementLoadCounts(tile.content); + --tileset._statistics.numberOfTilesWithContentReady; + tile.unloadContent(); + } - tileUnload.raiseEvent(tile); - replacementList.remove(node); - statistics.decrementLoadCounts(tile.content); - --statistics.numberOfTilesWithContentReady; + function destroyTile(tileset, tile) { + tileset._cache.unloadTile(tileset, tile, unloadTile); + tile.destroy(); } function unloadTiles(tileset) { - var trimTiles = tileset._trimTiles; - tileset._trimTiles = false; - - var replacementList = tileset._replacementList; - - var totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - var maximumMemoryUsageInBytes = tileset._maximumMemoryUsage * 1024 * 1024; - - // Traverse the list only to the sentinel since tiles/nodes to the - // right of the sentinel were used this frame. - // - // The sub-list to the left of the sentinel is ordered from LRU to MRU. - var sentinel = tileset._replacementSentinel; - var node = replacementList.head; - while ((node !== sentinel) && ((totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { - var tile = node.item; - node = node.next; - unloadTileFromCache(tileset, tile); - tile.unloadContent(); - totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - } + tileset._cache.unloadTiles(tileset, unloadTile); } /** @@ -1810,8 +1777,7 @@ define([ *

*/ Cesium3DTileset.prototype.trimLoadedTiles = function() { - // Defer to next frame so WebGL delete calls happen inside the render loop - this._trimTiles = true; + this._cache.trim(); }; /////////////////////////////////////////////////////////////////////////// @@ -1867,27 +1833,34 @@ define([ this._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, this._loadTimestamp) * 1000, 0.0); - this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail; + this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail && !this._allTilesAdditive; // Do not do out-of-core operations (new content requests, cache removal, // process new tiles) during the pick pass. var passes = frameState.passes; - var isPick = (passes.pick && !passes.render); - var outOfCore = !isPick; + var isRender = passes.render; + var isPick = passes.pick; + var outOfCore = isRender; var statistics = this._statistics; statistics.clear(); + if (this.dynamicScreenSpaceError) { + updateDynamicScreenSpaceError(this, frameState); + } + if (outOfCore) { - processTiles(this, frameState); + this._cache.reset(); } - if (this.dynamicScreenSpaceError) { - updateDynamicScreenSpaceError(this, frameState); + this._requestedTiles.length = 0; + Cesium3DTilesetTraversal.selectTiles(this, frameState); + + if (outOfCore) { + requestTiles(this); + processTiles(this, frameState); } - Cesium3DTilesetTraversal.selectTiles(this, frameState, outOfCore); - requestTiles(this, outOfCore); updateTiles(this, frameState); if (outOfCore) { diff --git a/Source/Scene/Cesium3DTilesetCache.js b/Source/Scene/Cesium3DTilesetCache.js new file mode 100644 index 000000000000..848a41da2529 --- /dev/null +++ b/Source/Scene/Cesium3DTilesetCache.js @@ -0,0 +1,79 @@ +define([ + '../Core/defined', + '../Core/DoublyLinkedList' + ], function( + defined, + DoublyLinkedList) { + 'use strict'; + + /** + * Stores tiles with content loaded. + * + * @private + */ + function Cesium3DTilesetCache() { + // [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache + // (sentinel, tail] -> tiles that were selected this frame + this._list = new DoublyLinkedList(); + this._sentinel = this._list.add(); + this._trimTiles = false; + } + + Cesium3DTilesetCache.prototype.reset = function() { + // Move sentinel node to the tail so, at the start of the frame, all tiles + // may be potentially replaced. Tiles are moved to the right of the sentinel + // when they are selected so they will not be replaced. + this._list.splice(this._list.tail, this._sentinel); + }; + + Cesium3DTilesetCache.prototype.touch = function(tile) { + var node = tile.cacheNode; + if (defined(node)) { + this._list.splice(this._sentinel, node); + } + }; + + Cesium3DTilesetCache.prototype.add = function(tile) { + if (!defined(tile.cacheNode)) { + tile.cacheNode = this._list.add(tile); + } + }; + + Cesium3DTilesetCache.prototype.unloadTile = function(tileset, tile, unloadCallback) { + var node = tile.cacheNode; + if (!defined(node)) { + return; + } + + this._list.remove(node); + tile.cacheNode = undefined; + unloadCallback(tileset, tile); + }; + + Cesium3DTilesetCache.prototype.unloadTiles = function(tileset, unloadCallback) { + var trimTiles = this._trimTiles; + this._trimTiles = false; + + var list = this._list; + + var maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024; + + // Traverse the list only to the sentinel since tiles/nodes to the + // right of the sentinel were used this frame. + // + // The sub-list to the left of the sentinel is ordered from LRU to MRU. + var sentinel = this._sentinel; + var node = list.head; + while ((node !== sentinel) && ((tileset.totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { + var tile = node.item; + node = node.next; + this.unloadTile(tileset, tile, unloadCallback); + } + }; + + Cesium3DTilesetCache.prototype.trim = function() { + this._trimTiles = true; + }; + + return Cesium3DTilesetCache; +}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3052b00ddb79..72447fbf8cda 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -1,23 +1,25 @@ define([ '../Core/CullingVolume', + '../Core/defaultValue', '../Core/defined', '../Core/freezeObject', '../Core/Intersect', '../Core/ManagedArray', '../Core/Math', '../Core/OrthographicFrustum', - './Cesium3DTileChildrenVisibility', + './Cesium3DTileOptimizationHint', './Cesium3DTileRefine', './SceneMode' ], function( CullingVolume, + defaultValue, defined, freezeObject, Intersect, ManagedArray, CesiumMath, OrthographicFrustum, - Cesium3DTileChildrenVisibility, + Cesium3DTileOptimizationHint, Cesium3DTileRefine, SceneMode) { 'use strict'; @@ -25,832 +27,610 @@ define([ /** * @private */ - var Cesium3DTilesetTraversal = {}; + function Cesium3DTilesetTraversal() { + } + + function isVisible(tile) { + return tile._visible && tile._inRequestVolume; + } + + var traversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + var emptyTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; - function selectTiles(tileset, frameState, outOfCore) { + var descendantTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + var selectionTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0, + ancestorStack : new ManagedArray(), + ancestorStackMaximumLength : 0 + }; + + var descendantSelectionDepth = 2; + + Cesium3DTilesetTraversal.selectTiles = function(tileset, frameState) { if (tileset.debugFreezeFrame) { return; } - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - - tileset._desiredTiles.length = 0; tileset._selectedTiles.length = 0; - tileset._requestedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; + tileset._emptyTiles.length = 0; tileset._hasMixedContent = false; - // Move sentinel node to the tail so, at the start of the frame, all tiles - // may be potentially replaced. Tiles are moved to the right of the sentinel - // when they are selected so they will not be replaced. - var replacementList = tileset._replacementList; - replacementList.splice(replacementList.tail, tileset._replacementSentinel); - var root = tileset._root; - root.updateTransform(tileset._modelMatrix); + updateTile(tileset, root, frameState); - if (!root.insideViewerRequestVolume(frameState)) { + // The root tile is not visible + if (!isVisible(root)) { return; } - root._distanceToCamera = root.distanceToTile(frameState); - - if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { - // The SSE of not rendering the tree is small enough that the tree does not need to be rendered + // The tileset doesn't meet the SSE requirement, therefore the tree does not need to be rendered + if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= tileset._maximumScreenSpaceError) { return; } - root._visibilityPlaneMask = root.visibility(frameState, CullingVolume.MASK_INDETERMINATE); - if (root._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { - return; - } - - loadTile(tileset, root, frameState, true); - - if (!tileset._skipLevelOfDetail) { - // just execute base traversal and add tiles to _desiredTiles - tileset._baseTraversal.execute(tileset, root, maximumScreenSpaceError, frameState, outOfCore); - var leaves = tileset._baseTraversal.leaves; - var length = leaves.length; - for (var i = 0; i < length; ++i) { - tileset._desiredTiles.push(leaves.get(i)); - } + if (!skipLevelOfDetail(tileset)) { + executeBaseTraversal(tileset, root, frameState); } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { - tileset._skipTraversal.execute(tileset, root, frameState, outOfCore); + executeSkipTraversal(tileset, root, frameState); } else { - // leaves of the base traversal is where we start the skip traversal - tileset._baseTraversal.leaves = tileset._skipTraversal.queue1; - - // load and select tiles without skipping up to tileset.baseScreenSpaceError - tileset._baseTraversal.execute(tileset, root, tileset.baseScreenSpaceError, frameState, outOfCore); - - // skip traversal starts from a prepopulated queue from the base traversal - tileset._skipTraversal.execute(tileset, undefined, frameState, outOfCore); + executeBaseAndSkipTraversal(tileset, root, frameState); } - // mark tiles for selection or their nearest loaded ancestor - markLoadedTilesForSelection(tileset, frameState, outOfCore); - - // sort selected tiles by distance to camera and call selectTile on each - // set tile._selectionDepth on all tiles - traverseAndSelect(tileset, root, frameState); + traversal.stack.trim(traversal.stackMaximumLength); + emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength); + descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); + selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); + selectionTraversal.ancestorStack.trim(selectionTraversal.ancestorStackMaximumLength); + }; - tileset._desiredTiles.trim(); + function executeBaseTraversal(tileset, root, frameState) { + var baseScreenSpaceError = tileset._maximumScreenSpaceError; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); } - var descendantStack = []; - - function markLoadedTilesForSelection(tileset, frameState, outOfCore) { - var tiles = tileset._desiredTiles; - var length = tiles.length; - for (var i = 0; i < length; ++i) { - var original = tiles.get(i); - - if (hasAdditiveContent(original)) { - original.selected = true; - original._selectedFrame = frameState.frameNumber; - continue; - } - - var loadedTile = original._ancestorWithLoadedContent; - if (original.hasRenderableContent && original.contentAvailable) { - loadedTile = original; - } - - if (defined(loadedTile)) { - loadedTile.selected = true; - loadedTile._selectedFrame = frameState.frameNumber; - } else { - // if no ancestors are ready, traverse down and select ready tiles to minimize empty regions - descendantStack.push(original); - while (descendantStack.length > 0) { - var tile = descendantStack.pop(); - var children = tile.children; - var childrenLength = children.length; - for (var j = 0; j < childrenLength; ++j) { - var child = children[j]; - touch(tileset, child, outOfCore); - if (child.contentAvailable) { - child.selected = true; - child._finalResolution = true; - child._selectedFrame = frameState.frameNumber; - } - if (child._depth - original._depth < 2) { // prevent traversing too far - if (!child.contentAvailable || child.refine === Cesium3DTileRefine.ADD) { - descendantStack.push(child); - } - } - } - } - } - } + function executeSkipTraversal(tileset, root, frameState) { + var baseScreenSpaceError = Number.MAX_VALUE; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); + traverseAndSelect(tileset, root, frameState); } - var scratchStack = []; - var scratchStack2 = []; - - /** - * Traverse the tree while tiles are visible and check if their selected frame is the current frame. - * If so, add it to a selection queue. - * Tiles are sorted near to far so we can take advantage of early Z. - * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. - * - * The reason for the preorder traversal is so that tiles can easily be marked with their - * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. - * This property is important for use in the stencil test because we want to render deeper tiles on top of their - * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. - * - * We want to select children before their ancestors because there is no guarantee on the relationship between - * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top - * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. @see {@link updateTiles} - * - * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the - * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be - * no deeper than 255. It is very, very unlikely this will cause a problem. - * - * NOTE: when the scene has inverted classification enabled, the stencil buffer will be masked to 4 bits. So, the - * selected tiles must be no deeper than 15. This is still very unlikely. - */ - function traverseAndSelect(tileset, root, frameState) { - var stack = scratchStack; - var ancestorStack = scratchStack2; - - var lastAncestor; - stack.push(root); - while (stack.length > 0 || ancestorStack.length > 0) { - if (ancestorStack.length > 0) { - var waitingTile = ancestorStack[ancestorStack.length - 1]; - if (waitingTile._stackLength === stack.length) { - ancestorStack.pop(); - if (waitingTile === lastAncestor) { - waitingTile._finalResolution = true; - } - selectTile(tileset, waitingTile, frameState); - continue; - } - } - - var tile = stack.pop(); - if (!defined(tile) || !isVisited(tile, frameState)) { - continue; - } - - var shouldSelect = tile.selected && tile._selectedFrame === frameState.frameNumber && tile.hasRenderableContent; - - var children = tile.children; - var childrenLength = children.length; - - children.sort(sortChildrenByDistanceToCamera); - - if (shouldSelect) { - if (tile.refine === Cesium3DTileRefine.ADD) { - tile._finalResolution = true; - selectTile(tileset, tile, frameState); - } else { - tile._selectionDepth = ancestorStack.length; - - if (tile._selectionDepth > 0) { - tileset._hasMixedContent = true; - } - - lastAncestor = tile; + function executeBaseAndSkipTraversal(tileset, root, frameState) { + var baseScreenSpaceError = Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); + var maximumScreenSpaceError = tileset.maximumScreenSpaceError; + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); + traverseAndSelect(tileset, root, frameState); + } - if (childrenLength === 0) { - tile._finalResolution = true; - selectTile(tileset, tile, frameState); - continue; - } + function skipLevelOfDetail(tileset) { + return tileset._skipLevelOfDetail; + } - ancestorStack.push(tile); - tile._stackLength = stack.length; - } - } + function addEmptyTile(tileset, tile) { + tileset._emptyTiles.push(tile); + } - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - stack.push(child); - } - } + function contentVisible(tile, frameState) { + return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || + (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } function selectTile(tileset, tile, frameState) { - // There may also be a tight box around just the tile's contents, e.g., for a city, we may be - // zoomed into a neighborhood and can cull the skyscrapers in the root tile. - if (tile.contentAvailable && ( - (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || - (tile.contentVisibility(frameState) !== Intersect.OUTSIDE) - )) { - tileset._selectedTiles.push(tile); - + if (contentVisible(tile, frameState)) { var tileContent = tile.content; if (tileContent.featurePropertiesDirty) { // A feature's property in this tile changed, the tile needs to be re-styled. tileContent.featurePropertiesDirty = false; tile.lastStyleTime = 0; // Force applying the style to this tile tileset._selectedTilesToStyle.push(tile); - } else if ((tile._lastSelectedFrameNumber !== frameState.frameNumber - 1) || tile.lastStyleTime === 0) { + } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { // Tile is newly selected; it is selected this frame, but was not selected last frame. + tile.lastStyleTime = 0; // Force applying the style to this tile tileset._selectedTilesToStyle.push(tile); } - tile._lastSelectedFrameNumber = frameState.frameNumber; + tile._selectedFrame = frameState.frameNumber; + tileset._selectedTiles.push(tile); } } - // PERFORMANCE_IDEA: is it worth exploiting frame-to-frame coherence in the sort, i.e., the - // list of children are probably fully or mostly sorted unless the camera moved significantly? - function sortChildrenByDistanceToCamera(a, b) { - // Sort by farthest child first since this is going on a stack - if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { - return b._centerZDepth - a._centerZDepth; + function selectDescendants(tileset, root, frameState) { + var stack = descendantTraversal.stack; + stack.push(root); + while (stack.length > 0) { + descendantTraversal.stackMaximumLength = Math.max(descendantTraversal.stackMaximumLength, stack.length); + var tile = stack.pop(); + var children = tile.children; + var childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (child.contentAvailable) { + updateTile(tileset, child, frameState); + touchTile(tileset, child, frameState); + selectTile(tileset, child, frameState); + } else if (child._depth - root._depth < descendantSelectionDepth) { + // Continue traversing, but not too far + stack.push(child); + } + } } - - return b._distanceToCamera - a._distanceToCamera; } - var emptyArray = freezeObject([]); - - function BaseTraversal() { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.stack = new ManagedArray(); - this.leaves = new ManagedArray(); - this.baseScreenSpaceError = undefined; - this.internalDFS = new InternalBaseTraversal(); - } - - BaseTraversal.prototype.execute = function(tileset, root, baseScreenSpaceError, frameState, outOfCore) { - this.tileset = tileset; - this.frameState = frameState; - this.outOfCore = outOfCore; - this.leaves.length = 0; - this.baseScreenSpaceError = Math.max(baseScreenSpaceError, this.tileset._maximumScreenSpaceError); - this.internalDFS.tileset = this.tileset; - this.internalDFS.frameState = this.frameState; - this.internalDFS.outOfCore = this.outOfCore; - this.internalDFS.baseScreenSpaceError = this.baseScreenSpaceError; - depthFirstSearch(root, this); - }; - - BaseTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); - } - }; - - BaseTraversal.prototype.visitEnd = function(tile) { - tile._lastVisitedFrame = this.frameState.frameNumber; - }; - - BaseTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var outOfCore = this.outOfCore; - var frameState = this.frameState; - if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { - return emptyArray; - } - - var children = tile.children; - var childrenLength = children.length; - var allReady = true; - var replacementWithContent = tile.refine === Cesium3DTileRefine.REPLACE && tile.hasRenderableContent; - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - loadTile(tileset, child, frameState, true); - touch(tileset, child, outOfCore); - - // content cannot be replaced until all of the nearest descendants with content are all loaded - if (replacementWithContent) { - if (!child.hasEmptyContent) { - allReady = allReady && child.contentAvailable; - } else { - allReady = allReady && this.internalDFS.execute(child); - } + function selectDesiredTile(tileset, tile, frameState) { + if (!skipLevelOfDetail(tileset)) { + if (tile.contentAvailable) { + // The tile can be selected right away and does not require traverseAndSelect + selectTile(tileset, tile, frameState); } + return; } - if (allReady) { - return children; + // If this tile is not loaded attempt to select its ancestor instead + var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; + if (defined(loadedTile)) { + // Tiles will actually be selected in traverseAndSelect + loadedTile._shouldSelect = true; + } else { + // If no ancestors are ready traverse down and select tiles to minimize empty regions. + // This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out. + selectDescendants(tileset, tile, frameState); } + } - return emptyArray; - }; + function visitTile(tileset, tile, frameState) { + ++tileset._statistics.visited; + tile._visitedFrame = frameState.frameNumber; + } - function baseUpdateAndCheckChildren(tileset, tile, baseScreenSpaceError, frameState) { - if (hasAdditiveContent(tile)) { - tileset._desiredTiles.push(tile); + function touchTile(tileset, tile, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { + // Prevents another pass from touching the frame again + return; } + tileset._cache.touch(tile); + tile._touchedFrame = frameState.frameNumber; + } - // Stop traversal on the subtree since it will be destroyed - if (tile.hasTilesetContent && tile.contentExpired) { - return false; + function getPriority(tileset, tile) { + // If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner. + // Additive tiles are prioritized by distance because it subjectively looks better. + // Replacement tiles are prioritized by screen space error. + // A tileset that has both additive and replacement tiles may not prioritize tiles as effectively since SSE and distance + // are different types of values. Maybe all priorities need to be normalized to 0-1 range. + if (tile.refine === Cesium3DTileRefine.ADD) { + return tile._distanceToCamera; } + var parent = tile.parent; + var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); + var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; + var rootScreenSpaceError = tileset._root._screenSpaceError; + return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. root tile is highest priority) + } - // stop traversal when we've attained the desired level of error - if (tile._screenSpaceError <= baseScreenSpaceError && !tile.hasTilesetContent) { - // update children so the leaf handler can check if any are visible for the children union bound optimization - updateChildren(tile, frameState); - return false; + function loadTile(tileset, tile, frameState) { + if (hasUnloadedContent(tile) || tile.contentExpired) { + tile._requestedFrame = frameState.frameNumber; + tile._priority = getPriority(tileset, tile); + tileset._requestedTiles.push(tile); } + } - var childrenVisibility = updateChildren(tile, frameState); - var showAdditive = tile.refine === Cesium3DTileRefine.ADD; - var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; + function getScreenSpaceError(tileset, geometricError, tile, frameState) { + if (geometricError === 0.0) { + // Leaf tiles do not have any error so save the computation + return 0.0; + } - return showAdditive || showReplacement || tile.hasTilesetContent || !defined(tile._ancestorWithContent); - } + var camera = frameState.camera; + var frustum = camera.frustum; + var context = frameState.context; + var height = context.drawingBufferHeight; - BaseTraversal.prototype.shouldVisit = function(tile) { - return isVisible(tile._visibilityPlaneMask); - }; + var error; + if (frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum) { + if (defined(frustum._offCenterFrustum)) { + frustum = frustum._offCenterFrustum; + } + var width = context.drawingBufferWidth; + var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); + error = geometricError / pixelSize; + } else { + // Avoid divide by zero when viewer is inside the tile + var distance = Math.max(tile._distanceToCamera, CesiumMath.EPSILON7); + var sseDenominator = camera.frustum.sseDenominator; + error = (geometricError * height) / (distance * sseDenominator); - BaseTraversal.prototype.leafHandler = function(tile) { - // if skipLevelOfDetail is off, leaves of the base traversal get pushed to tileset._desiredTiles. additive tiles have already been pushed - if (this.tileset._skipLevelOfDetail || !hasAdditiveContent(tile)) { - if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { - ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; - return; + if (tileset.dynamicScreenSpaceError) { + var density = tileset._dynamicScreenSpaceErrorComputedDensity; + var factor = tileset.dynamicScreenSpaceErrorFactor; + var dynamicError = CesiumMath.fog(distance, density) * factor; + error -= dynamicError; } - this.leaves.push(tile); } - }; - function InternalBaseTraversal() { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.baseScreenSpaceError = undefined; - this.stack = new ManagedArray(); - this.allLoaded = undefined; + return error; } - InternalBaseTraversal.prototype.execute = function(root) { - this.allLoaded = true; - depthFirstSearch(root, this); - return this.allLoaded; - }; - - InternalBaseTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); + function updateVisibility(tileset, tile, frameState) { + if (tile._updatedVisibilityFrame === frameState.frameNumber) { + // Return early if visibility has already been checked during the traversal. + // The visibility may have already been checked if the cullWithChildrenBounds optimization is used. + return; } - }; - - InternalBaseTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; - - // Continue traversing until we have renderable content. We want the first descendants with content of the root to load - InternalBaseTraversal.prototype.shouldVisit = function(tile) { - return !tile.hasRenderableContent && isVisible(tile._visibilityPlaneMask); - }; - - InternalBaseTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var frameState = this.frameState; - var outOfCore = this.outOfCore; - if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { - return emptyArray; - } + var parent = tile.parent; + var parentTransform = defined(parent) ? parent.computedTransform : tileset._modelMatrix; + var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; + + tile.updateTransform(parentTransform); + tile._distanceToCamera = tile.distanceToTile(frameState); + tile._centerZDepth = tile.distanceToTileCenter(frameState); + tile._screenSpaceError = getScreenSpaceError(tileset, tile.geometricError, tile, frameState); + tile._visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test + tile._visible = tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + tile._inRequestVolume = tile.insideViewerRequestVolume(frameState); + tile._updatedVisibilityFrame = frameState.frameNumber; + } + function anyChildrenVisible(tileset, tile, frameState) { + var anyVisible = false; var children = tile.children; - var childrenLength = children.length; - for (var i = 0; i < childrenLength; ++i) { + var length = children.length; + for (var i = 0; i < length; ++i) { var child = children[i]; - loadTile(tileset, child, frameState, true); - touch(tileset, child, outOfCore); - if (!tile.contentAvailable) { - this.allLoaded = false; - } + updateVisibility(tileset, child, frameState); + anyVisible = anyVisible || isVisible(child); } - return children; - }; - - InternalBaseTraversal.prototype.updateAndCheckChildren = BaseTraversal.prototype.updateAndCheckChildren; - - function SkipTraversal(options) { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.queue1 = new ManagedArray(); - this.queue2 = new ManagedArray(); - this.internalDFS = new InternalSkipTraversal(options.selectionHeuristic); - this.maxChildrenLength = 0; - this.scratchQueue = new ManagedArray(); + return anyVisible; } - SkipTraversal.prototype.execute = function(tileset, root, frameState, outOfCore) { - this.tileset = tileset; - this.frameState = frameState; - this.outOfCore = outOfCore; - this.internalDFS.frameState = frameState; - this.internalDFS.outOfCore = outOfCore; - - this.maxChildrenLength = 0; - breadthFirstSearch(root, this); - this.queue1.length = 0; - this.queue2.length = 0; - this.scratchQueue.length = 0; - this.scratchQueue.trim(this.maxChildrenLength); - }; + function updateTileVisibility(tileset, tile, frameState) { + updateVisibility(tileset, tile, frameState); - SkipTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); + if (!isVisible(tile)) { + return; } - }; - - SkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; - - SkipTraversal.prototype.getChildren = function(tile) { - this.scratchQueue.length = 0; - this.internalDFS.execute(tile, this.scratchQueue); - this.maxChildrenLength = Math.max(this.maxChildrenLength, this.scratchQueue.length); - return this.scratchQueue; - }; - SkipTraversal.prototype.leafHandler = function(tile) { - // additive tiles have already been pushed - if (!hasAdditiveContent(tile) && !isVisited(tile, this.frameState)) { - this.tileset._desiredTiles.push(tile); + // Use parent's geometric error with child's box to see if the tile already meet the SSE + var parent = tile.parent; + if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= tileset._maximumScreenSpaceError) { + tile._visible = false; + return; } - }; - - function InternalSkipTraversal(selectionHeuristic) { - this.selectionHeuristic = selectionHeuristic; - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.root = undefined; - this.queue = undefined; - this.stack = new ManagedArray(); - } - - InternalSkipTraversal.prototype.execute = function(root, queue) { - this.tileset = root._tileset; - this.root = root; - this.queue = queue; - depthFirstSearch(root, this); - }; - InternalSkipTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); + // Optimization - if none of the tile's children are visible then this tile isn't visible + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var useOptimization = tile._optimChildrenWithinParent === Cesium3DTileOptimizationHint.USE_OPTIMIZATION; + var hasChildren = tile.children.length > 0; + if (replace && useOptimization && hasChildren) { + if (!anyChildrenVisible(tileset, tile, frameState)) { + ++tileset._statistics.numberOfTilesCulledWithChildrenUnion; + tile._visible = false; + return; + } } - }; + } - InternalSkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; + function updateTile(tileset, tile, frameState) { + updateTileVisibility(tileset, tile, frameState); + tile.updateExpiration(); - InternalSkipTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + tile._shouldSelect = false; + tile._finalResolution = true; + tile._ancestorWithContent = undefined; + tile._ancestorWithContentAvailable = undefined; - // Stop traversal on the subtree since it will be destroyed - if (tile.hasTilesetContent && tile.contentExpired) { - return emptyArray; + var parent = tile.parent; + if (defined(parent)) { + // ancestorWithContent is an ancestor that has content or has the potential to have + // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. + // ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded. + var hasContent = !hasUnloadedContent(parent) || (parent._requestedFrame === frameState.frameNumber); + tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; + tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; } + } - if (!tile.hasTilesetContent) { - if (tile.refine === Cesium3DTileRefine.ADD) { - // Always load additive tiles - loadTile(tileset, tile, this.frameState, true); - if (hasAdditiveContent(tile)) { - tileset._desiredTiles.push(tile); - } - } + function hasEmptyContent(tile) { + return tile.hasEmptyContent || tile.hasTilesetContent; + } - // stop traversal when we've attained the desired level of error - if (tile._screenSpaceError <= maximumScreenSpaceError) { - updateChildren(tile, this.frameState); - return emptyArray; - } + function hasUnloadedContent(tile) { + return !hasEmptyContent(tile) && tile.contentUnloaded; + } - // if we have reached the skipping threshold without any loaded ancestors, return empty so this tile is loaded - if ( - (!tile.hasEmptyContent && tile.contentUnloaded) && - defined(tile._ancestorWithLoadedContent) && - this.selectionHeuristic(tileset, tile._ancestorWithLoadedContent, tile)) { - updateChildren(tile, this.frameState); - return emptyArray; - } + function reachedSkippingThreshold(tileset, tile) { + var ancestor = tile._ancestorWithContent; + return !tileset.immediatelyLoadDesiredLevelOfDetail && + defined(ancestor) && + (tile._screenSpaceError < (ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor)) && + (tile._depth > (ancestor._depth + tileset.skipLevels)); + } + + function sortChildrenByDistanceToCamera(a, b) { + // Sort by farthest child first since this is going on a stack + if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { + return b._centerZDepth - a._centerZDepth; } - var childrenVisibility = updateChildren(tile, this.frameState); - var showAdditive = tile.refine === Cesium3DTileRefine.ADD && tile._screenSpaceError > maximumScreenSpaceError; - var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; + return b._distanceToCamera - a._distanceToCamera; + } - // at least one child is visible, but is not in request volume. the parent must be selected - if (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME && tile.refine === Cesium3DTileRefine.REPLACE) { - this.tileset._desiredTiles.push(tile); - } + function updateAndPushChildren(tileset, tile, stack, frameState) { + var i; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var children = tile.children; + var length = children.length; - if (showAdditive || showReplacement || tile.hasTilesetContent) { - var children = tile.children; - var childrenLength = children.length; - for (var i = 0; i < childrenLength; ++i) { - touch(tileset, children[i], this.outOfCore); - } - return children; + for (i = 0; i < length; ++i) { + updateTile(tileset, children[i], frameState); } - return emptyArray; - }; + // Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail + children.sort(sortChildrenByDistanceToCamera); - InternalSkipTraversal.prototype.shouldVisit = function(tile) { - return isVisibleAndMeetsSSE(this.tileset, tile, this.frameState); - }; + // For traditional replacement refinement only refine if all children are loaded. + // Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space. + var checkRefines = !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(tile); + var refines = true; - InternalSkipTraversal.prototype.leafHandler = function(tile) { - if (tile !== this.root) { - if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { - ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; - return; + var anyChildrenVisible = false; + for (i = 0; i < length; ++i) { + var child = children[i]; + if (isVisible(child)) { + stack.push(child); + anyChildrenVisible = true; + } else if (checkRefines || tileset.loadSiblings) { + // Keep non-visible children loaded since they are still needed before the parent can refine. + // Or loadSiblings is true so always load tiles regardless of visibility. + loadTile(tileset, child, frameState); + touchTile(tileset, child, frameState); } - if (!tile.hasEmptyContent) { - if (this.tileset.loadSiblings) { - var parent = tile.parent; - var tiles = parent.children; - var length = tiles.length; - for (var i = 0; i < length; ++i) { - loadTile(this.tileset, tiles[i], this.frameState, false); - touch(this.tileset, tiles[i], this.outOfCore); - } + if (checkRefines) { + var childRefines; + if (!child._inRequestVolume) { + childRefines = false; + } else if (hasEmptyContent(child)) { + childRefines = executeEmptyTraversal(tileset, child, frameState); } else { - loadTile(this.tileset, tile, this.frameState, true); - touch(this.tileset, tile, this.outOfCore); + childRefines = child.contentAvailable; } + refines = refines && childRefines; } - this.queue.push(tile); - } else if (!hasAdditiveContent(tile)) { - // additive tiles have already been pushed - this.tileset._desiredTiles.push(tile); } - }; - function updateChildren(tile, frameState) { - if (isVisited(tile, frameState)) { - return tile._childrenVisibility; + if (!anyChildrenVisible) { + refines = false; } - var children = tile.children; - - updateTransforms(children, tile.computedTransform); - computeDistanceToCamera(children, frameState); - - return computeChildrenVisibility(tile, frameState); - } - - function isVisited(tile, frameState) { - // because the leaves of one tree traversal are the root of the subsequent traversal, avoid double visitation - return tile._lastVisitedFrame === frameState.frameNumber; - } - - function visitTile(tileset, tile, frameState, outOfCore) { - ++tileset._statistics.visited; - tile.selected = false; - tile._finalResolution = false; - computeSSE(tile, frameState); - touch(tileset, tile, outOfCore); - tile.updateExpiration(); - tile._ancestorWithContent = undefined; - tile._ancestorWithLoadedContent = undefined; - var parent = tile.parent; - if (defined(parent)) { - var replace = parent.refine === Cesium3DTileRefine.REPLACE; - tile._ancestorWithContent = (replace && parent.hasRenderableContent) ? parent : parent._ancestorWithContent; - tile._ancestorWithLoadedContent = (replace && parent.hasRenderableContent && parent.contentAvailable) ? parent : parent._ancestorWithLoadedContent; - } + return refines; } - function touch(tileset, tile, outOfCore) { - if (!outOfCore) { - return; + function inBaseTraversal(tileset, tile, baseScreenSpaceError) { + if (!skipLevelOfDetail(tileset)) { + return true; } - var node = tile.replacementNode; - if (defined(node)) { - tileset._replacementList.splice(tileset._replacementSentinel, node); + if (tileset.immediatelyLoadDesiredLevelOfDetail) { + return false; } - } - - function computeSSE(tile, frameState) { - if (tile._screenSpaceErrorComputedFrame !== frameState.frameNumber) { - tile._screenSpaceErrorComputedFrame = frameState.frameNumber; - tile._screenSpaceError = getScreenSpaceError(tile._tileset, tile.geometricError, tile, frameState); + if (!defined(tile._ancestorWithContent)) { + // Include root or near-root tiles in the base traversal so there is something to select up to + return true; } - } - - function checkAdditiveVisibility(tileset, tile, frameState) { - if (defined(tile.parent) && (tile.parent.refine === Cesium3DTileRefine.ADD)) { - return isVisibleAndMeetsSSE(tileset, tile, frameState); + if (tile._screenSpaceError === 0.0) { + // If a leaf, use parent's SSE + return tile.parent._screenSpaceError > baseScreenSpaceError; } - return true; + return tile._screenSpaceError > baseScreenSpaceError; } - function loadTile(tileset, tile, frameState, checkVisibility) { - if ((tile.contentUnloaded || tile.contentExpired) && tile._requestedFrame !== frameState.frameNumber) { - if (!checkVisibility || checkAdditiveVisibility(tileset, tile, frameState)) { - tile._requestedFrame = frameState.frameNumber; - tileset._requestedTiles.push(tile); - } - } - } + function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { + // Depth-first traversal that traverses all visible tiles and marks tiles for selection. + // If skipLevelOfDetail is off then a tile does not refine until all children are loaded. + // This is the traditional replacement refinement approach and is called the base traversal. + // Tiles that have a greater screen space error than the base screen space error are part of the base traversal, + // all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree + // and rendering children and parent tiles simultaneously. + var stack = traversal.stack; + stack.push(root); - function computeChildrenVisibility(tile, frameState) { - var flag = Cesium3DTileChildrenVisibility.NONE; - var children = tile.children; - var childrenLength = children.length; - var visibilityPlaneMask = tile._visibilityPlaneMask; - for (var k = 0; k < childrenLength; ++k) { - var child = children[k]; + while (stack.length > 0) { + traversal.stackMaximumLength = Math.max(traversal.stackMaximumLength, stack.length); - var visibilityMask = child.visibility(frameState, visibilityPlaneMask); + var tile = stack.pop(); + var baseTraversal = inBaseTraversal(tileset, tile, baseScreenSpaceError); + var add = tile.refine === Cesium3DTileRefine.ADD; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var children = tile.children; + var childrenLength = children.length; + var parent = tile.parent; + var parentRefines = !defined(parent) || parent._refines; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); + var refines = false; + + if (tile.hasTilesetContent && tile.contentExpired) { + // Don't traverse expired subtree because it will be destroyed + traverse = false; + } - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE; + if (traverse) { + refines = updateAndPushChildren(tileset, tile, stack, frameState) && parentRefines; } - if (!child.insideViewerRequestVolume(frameState)) { - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME; - } - visibilityMask = CullingVolume.MASK_OUTSIDE; - } else { - flag |= Cesium3DTileChildrenVisibility.IN_REQUEST_VOLUME; - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME; + if (hasEmptyContent(tile)) { + // Add empty tile just to show its debug bounding volume + // If the tile has tileset content load the external tileset + addEmptyTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); + } else if (add) { + // Additive tiles are always loaded and selected + selectDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); + } else if (replace) { + if (baseTraversal) { + // Always load tiles in the base traversal + // Select tiles that can't refine further + loadTile(tileset, tile, frameState); + if (!refines && parentRefines) { + selectDesiredTile(tileset, tile, frameState); + } + } else { + // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. + // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. + if (!refines) { // eslint-disable-line + selectDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); + } else if (reachedSkippingThreshold(tileset, tile)) { + loadTile(tileset, tile, frameState); + } } } - child._visibilityPlaneMask = visibilityMask; + visitTile(tileset, tile, frameState); + touchTile(tileset, tile, frameState); + tile._refines = refines; + tile._updatedVisibilityFrame = 0; // Reset so visibility is checked during the next pass } + } - tile._childrenVisibility = flag; + function executeEmptyTraversal(tileset, root, frameState) { + // Depth-first traversal that checks if all nearest descendants with content are loaded. Ignores visibility. + var allDescendantsLoaded = true; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + var stack = emptyTraversal.stack; + stack.push(root); - return flag; - } + while (stack.length > 0) { + emptyTraversal.stackMaximumLength = Math.max(emptyTraversal.stackMaximumLength, stack.length); - function getScreenSpaceError(tileset, geometricError, tile, frameState) { - if (geometricError === 0.0) { - // Leaf tiles do not have any error so save the computation - return 0.0; - } + var tile = stack.pop(); + var children = tile.children; + var childrenLength = children.length; - // Avoid divide by zero when viewer is inside the tile - var camera = frameState.camera; - var frustum = camera.frustum; - var context = frameState.context; - var height = context.drawingBufferHeight; + // Only traverse if the tile is empty - traversal stop at descendants with content + var traverse = hasEmptyContent(tile) && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - var error; - if (frameState.mode === SceneMode.SCENE2D || frustum instanceof OrthographicFrustum) { - if (defined(frustum._offCenterFrustum)) { - frustum = frustum._offCenterFrustum; + // Traversal stops but the tile does not have content yet. + // There will be holes if the parent tries to refine to its children, so don't refine. + if (!traverse && !tile.contentAvailable) { + allDescendantsLoaded = false; } - var width = context.drawingBufferWidth; - var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); - error = geometricError / pixelSize; - } else { - var distance = Math.max(tile._distanceToCamera, CesiumMath.EPSILON7); - var sseDenominator = camera.frustum.sseDenominator; - error = (geometricError * height) / (distance * sseDenominator); - if (tileset.dynamicScreenSpaceError) { - var density = tileset._dynamicScreenSpaceErrorComputedDensity; - var factor = tileset.dynamicScreenSpaceErrorFactor; - var dynamicError = CesiumMath.fog(distance, density) * factor; - error -= dynamicError; + updateTile(tileset, tile, frameState); + if (!isVisible(tile)) { + // Load tiles that aren't visible since they are still needed for the parent to refine + loadTile(tileset, tile, frameState); + touchTile(tileset, tile, frameState); } - } - - return error; - } - - function computeDistanceToCamera(children, frameState) { - var length = children.length; - for (var i = 0; i < length; ++i) { - var child = children[i]; - child._distanceToCamera = child.distanceToTile(frameState); - child._centerZDepth = child.distanceToTileCenter(frameState); - } - } - function updateTransforms(children, parentTransform) { - var length = children.length; - for (var i = 0; i < length; ++i) { - var child = children[i]; - child.updateTransform(parentTransform); + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + stack.push(child); + } + } } - } - function isVisible(visibilityPlaneMask) { - return visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + return allDescendantsLoaded; } - function isVisibleAndMeetsSSE(tileset, tile, frameState) { + /** + * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. + * This is a preorder traversal so children tiles are selected before ancestor tiles. + * + * The reason for the preorder traversal is so that tiles can easily be marked with their + * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. + * This property is important for use in the stencil test because we want to render deeper tiles on top of their + * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. + * + * We want to select children before their ancestors because there is no guarantee on the relationship between + * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top + * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. + * + * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the + * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be + * no deeper than 255. It is very, very unlikely this will cause a problem. + * + * NOTE: when the scene has inverted classification enabled, the stencil buffer will be masked to 4 bits. So, the + * selected tiles must be no deeper than 15. This is still very unlikely. + */ + function traverseAndSelect(tileset, root, frameState) { var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - var parent = tile.parent; - if (!defined(parent)) { - return isVisible(tile._visibilityPlaneMask); - } - var showAdditive = parent.refine === Cesium3DTileRefine.ADD && parent._screenSpaceError > maximumScreenSpaceError; - - return isVisible(tile._visibilityPlaneMask) && (!showAdditive || getScreenSpaceError(tileset, parent.geometricError, tile, frameState) > maximumScreenSpaceError); - } - - function childrenAreVisible(tile) { - // optimization does not apply for additive refinement - return tile.refine === Cesium3DTileRefine.ADD || tile.children.length === 0 || tile._childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE !== 0; - } - - function hasAdditiveContent(tile) { - return tile.refine === Cesium3DTileRefine.ADD && tile.hasRenderableContent; - } - - function depthFirstSearch(root, options) { - var stack = options.stack; + var stack = selectionTraversal.stack; + var ancestorStack = selectionTraversal.ancestorStack; + var lastAncestor; - if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { - stack.push(root); - } + stack.push(root); - var maxLength = 0; - while (stack.length > 0) { - maxLength = Math.max(maxLength, stack.length); + while (stack.length > 0 || ancestorStack.length > 0) { + selectionTraversal.stackMaximumLength = Math.max(selectionTraversal.stackMaximumLength, stack.length); + selectionTraversal.ancestorStackMaximumLength = Math.max(selectionTraversal.ancestorStackMaximumLength, ancestorStack.length); - var tile = stack.pop(); - options.visitStart(tile); - var children = options.getChildren(tile); - var isNativeArray = !defined(children.get); - var length = children.length; - for (var i = 0; i < length; ++i) { - var child = isNativeArray ? children[i] : children.get(i); - - if (!defined(options.shouldVisit) || options.shouldVisit(child)) { - stack.push(child); + if (ancestorStack.length > 0) { + var waitingTile = ancestorStack.peek(); + if (waitingTile._stackLength === stack.length) { + ancestorStack.pop(); + if (waitingTile !== lastAncestor) { + waitingTile._finalResolution = false; + } + selectTile(tileset, waitingTile, frameState); + continue; } } - if (length === 0 && defined(options.leafHandler)) { - options.leafHandler(tile); + var tile = stack.pop(); + if (!defined(tile)) { + // stack is empty but ancestorStack isn't + continue; } - options.visitEnd(tile); - } - stack.trim(maxLength); - } - - function breadthFirstSearch(root, options) { - var queue1 = options.queue1; - var queue2 = options.queue2; - - if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { - queue1.push(root); - } + var add = tile.refine === Cesium3DTileRefine.ADD; + var shouldSelect = tile._shouldSelect; + var children = tile.children; + var childrenLength = children.length; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - var maxLength = 0; - while (queue1.length > 0) { - var length = queue1.length; - maxLength = Math.max(maxLength, length); - - for (var i = 0; i < length; ++i) { - var tile = queue1.get(i); - options.visitStart(tile); - var children = options.getChildren(tile); - var isNativeArray = !defined(children.get); - var childrenLength = children.length; - for (var j = 0; j < childrenLength; ++j) { - var child = isNativeArray ? children[j] : children.get(j); - - if (!defined(options.shouldVisit) || options.shouldVisit(child)) { - queue2.push(child); + if (shouldSelect) { + if (add) { + selectTile(tileset, tile, frameState); + } else { + tile._selectionDepth = ancestorStack.length; + if (tile._selectionDepth > 0) { + tileset._hasMixedContent = true; } + lastAncestor = tile; + if (!traverse) { + selectTile(tileset, tile, frameState); + continue; + } + ancestorStack.push(tile); + tile._stackLength = stack.length; } + } - if (childrenLength === 0 && defined(options.leafHandler)) { - options.leafHandler(tile); + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (isVisible(child)) { + stack.push(child); + } } - options.visitEnd(tile); } - - queue1.length = 0; - var temp = queue1; - queue1 = queue2; - queue2 = temp; - options.queue1 = queue1; - options.queue2 = queue2; } - - queue1.length = 0; - queue2.length = 0; - - queue1.trim(maxLength); - queue2.trim(maxLength); } - Cesium3DTilesetTraversal.selectTiles = selectTiles; - - Cesium3DTilesetTraversal.BaseTraversal = BaseTraversal; - - Cesium3DTilesetTraversal.SkipTraversal = SkipTraversal; - return Cesium3DTilesetTraversal; }); diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index 48ac2003f723..52686d5a3d88 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -352,6 +352,7 @@ define([ }); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) diff --git a/Source/Scene/TileBoundingSphere.js b/Source/Scene/TileBoundingSphere.js index ffc98d1245ad..278e11ad8a51 100644 --- a/Source/Scene/TileBoundingSphere.js +++ b/Source/Scene/TileBoundingSphere.js @@ -151,6 +151,7 @@ define([ var modelMatrix = Matrix4.fromTranslation(this.center, new Matrix4.clone(Matrix4.IDENTITY)); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) diff --git a/Source/Scene/TileOrientedBoundingBox.js b/Source/Scene/TileOrientedBoundingBox.js index bf09771654a6..c1f0ef277694 100644 --- a/Source/Scene/TileOrientedBoundingBox.js +++ b/Source/Scene/TileOrientedBoundingBox.js @@ -134,6 +134,7 @@ define([ var modelMatrix = Matrix4.fromRotationTranslation(this.boundingVolume.halfAxes, this.boundingVolume.center); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js index e4d237fce688..d80ba01ddb0e 100644 --- a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js @@ -71,7 +71,7 @@ define([ return ''; } - var statistics = tileset.statistics; + var statistics = isPick ? tileset._statisticsLastPick : tileset._statisticsLastColor; // Since the pick pass uses a smaller frustum around the pixel of interest, // the statistics will be different than the normal render pass. @@ -81,7 +81,7 @@ define([ '
  • Visited: ' + statistics.visited.toLocaleString() + '
  • ' + // Number of commands returned is likely to be higher than the number of tiles selected // because of tiles that create multiple commands. - '
  • Selected: ' + tileset._selectedTiles.length.toLocaleString() + '
  • ' + + '
  • Selected: ' + statistics.selected.toLocaleString() + '
  • ' + // Number of commands executed is likely to be higher because of commands overlapping // multiple frustums. '
  • Commands: ' + statistics.numberOfCommands.toLocaleString() + '
  • '; diff --git a/Specs/Cesium3DTilesTester.js b/Specs/Cesium3DTilesTester.js index 7180ee1a0a05..500b9aa877d4 100644 --- a/Specs/Cesium3DTilesTester.js +++ b/Specs/Cesium3DTilesTester.js @@ -97,6 +97,7 @@ define([ scene.renderForSpecs(); return tileset.tilesLoaded; }).then(function() { + scene.renderForSpecs(); return tileset; }); }; diff --git a/Specs/Core/ManagedArraySpec.js b/Specs/Core/ManagedArraySpec.js index 94c1c5b32a38..c050bb8f6013 100644 --- a/Specs/Core/ManagedArraySpec.js +++ b/Specs/Core/ManagedArraySpec.js @@ -55,6 +55,16 @@ defineSuite([ expect(array.length).toEqual(6); }); + it('peeks at the last element of the array', function() { + var array = new ManagedArray(); + expect(array.peek()).toBeUndefined(); + array.push(0); + expect(array.peek()).toBe(0); + array.push(1); + array.push(2); + expect(array.peek()).toBe(2); + }); + it('can push values', function() { var array = new ManagedArray(); var length = 10; diff --git a/Specs/Scene/Cesium3DTileSpec.js b/Specs/Scene/Cesium3DTileSpec.js index a0699cedf8fd..590031c45c21 100644 --- a/Specs/Scene/Cesium3DTileSpec.js +++ b/Specs/Scene/Cesium3DTileSpec.js @@ -317,6 +317,7 @@ defineSuite([ var scene; beforeEach(function() { scene = createScene(); + scene.frameState.passes.render = true; }); afterEach(function() { diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index b3b212c829d5..8a018af4e9fe 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -185,12 +185,6 @@ defineSuite([ scene.camera.moveDown(200.0); } - function viewBottomRight() { - viewAllTiles(); - scene.camera.moveRight(200.0); - scene.camera.moveDown(200.0); - } - function viewInstances() { setZoom(30.0); } @@ -199,6 +193,10 @@ defineSuite([ setZoom(5.0); } + function isSelected(tileset, tile) { + return tileset._selectedTiles.indexOf(tile) > -1; + } + it('throws with undefined url', function() { expect(function() { return new Cesium3DTileset(); @@ -986,9 +984,7 @@ defineSuite([ }); it('replacement refinement - selects root when sse is not met and children are not ready', function() { - // Set view so that only root tile is loaded initially viewRootOnly(); - return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; @@ -1008,26 +1004,28 @@ defineSuite([ }); it('replacement refinement - selects tile when inside viewer request volume', function() { - return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + var options = { + skipLevelOfDetail : false + }; + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl, options).then(function(tileset) { var statistics = tileset._statistics; var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; - // Force root tile to always not meet SSE since this is just checking the request volume - tileset.maximumScreenSpaceError = 0.0; + root.hasEmptyContent = false; // mock content + tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume // Renders all 5 tiles setZoom(20.0); scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(5); - expect(root.selected).toBe(false); + expect(isSelected(tileset, root)).toBe(false); // No longer renders the tile with a request volume setZoom(1500.0); - root.hasRenderableContent = true; // mock content scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(4); - expect(root.selected).toBe(true); // one child is no longer selected. root is chosen instead + expect(isSelected(tileset, root)).toBe(true); // one child is no longer selected. root is chosen instead }); }); @@ -1038,24 +1036,23 @@ defineSuite([ // E E // C C C C // - viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; viewAllTiles(); scene.renderForSpecs(); var statistics = tileset._statistics; var root = tileset._root; - return when.join(root.children[0].contentReadyPromise, root.children[1].contentReadyPromise).then(function() { - // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable - expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded - expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content - expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren + // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable + expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded + expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content + expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren - return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { - expect(statistics.numberOfCommands).toEqual(4); // Render children - }); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(4); // Render children }); }); }); @@ -1071,6 +1068,7 @@ defineSuite([ viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement2Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; var statistics = tileset._statistics; return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { expect(statistics.numberOfCommands).toEqual(1); @@ -1096,6 +1094,7 @@ defineSuite([ viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; var statistics = tileset._statistics; var root = tileset._root; expect(statistics.numberOfCommands).toEqual(1); @@ -1109,7 +1108,7 @@ defineSuite([ expect(statistics.numberOfPendingRequests).toEqual(4); // Loading child content tiles return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { - expect(root.selected).toEqual(false); + expect(isSelected(tileset, root)).toEqual(false); expect(statistics.numberOfCommands).toEqual(4); // Render child content tiles }); }); @@ -1147,7 +1146,7 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(0); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); @@ -1166,7 +1165,7 @@ defineSuite([ expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); @@ -1186,14 +1185,17 @@ defineSuite([ expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); }); }); }); - it('does select visibile tiles with visible children failing request volumes', function() { + it('does select visible tiles with visible children failing request volumes', function() { + var options = { + cullWithChildrenBounds : false + }; viewRootOnly(); - return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl, options).then(function(tileset) { var root = tileset._root; var childRoot = root.children[0]; @@ -1205,11 +1207,11 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(1); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); }); }); - it('does select visibile tiles with visible children passing request volumes', function() { + it('does select visible tiles with visible children passing request volumes', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { var root = tileset._root; var childRoot = root.children[0]; @@ -1225,12 +1227,12 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(1); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); childRoot.geometricError = 200; scene.renderForSpecs(); expect(tileset._selectedTiles.length).toEqual(4); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); }); @@ -2303,14 +2305,14 @@ defineSuite([ it('Unloads cached tiles in a tileset with external tileset JSON file using maximumMemoryUsage', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { var statistics = tileset._statistics; - var replacementList = tileset._replacementList; + var cacheList = tileset._cache._list; tileset.maximumMemoryUsage = 0.02; scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. + expect(cacheList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. // Zoom out so only root tile is needed to meet SSE. This unloads // all tiles except the root and one of the b3dm children @@ -2319,7 +2321,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(1); expect(statistics.numberOfTilesWithContentReady).toEqual(2); - expect(replacementList.length - 1).toEqual(2); + expect(cacheList.length - 1).toEqual(2); // Reset camera so all tiles are reloaded viewAllTiles(); @@ -2328,7 +2330,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); + expect(cacheList.length - 1).toEqual(5); }); }); }); @@ -2556,7 +2558,6 @@ defineSuite([ it('adds stencil clear command first when unresolved', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { - tileset._root.children[0].children[0].children[0].unloadContent(); tileset._root.children[0].children[0].children[1].unloadContent(); tileset._root.children[0].children[0].children[2].unloadContent(); @@ -2570,7 +2571,6 @@ defineSuite([ it('creates duplicate backface commands', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { - var statistics = tileset._statistics; var root = tileset._root; @@ -2583,9 +2583,9 @@ defineSuite([ // 2 for root tile, 1 for child, 1 for stencil clear // Tiles that are marked as finalResolution, including leaves, do not create back face commands expect(statistics.numberOfCommands).toEqual(4); - expect(root.selected).toBe(true); + expect(isSelected(tileset, root)).toBe(true); expect(root._finalResolution).toBe(false); - expect(root.children[0].children[0].children[3].selected).toBe(true); + expect(isSelected(tileset, root.children[0].children[0].children[3])).toBe(true); expect(root.children[0].children[0].children[3]._finalResolution).toBe(true); expect(tileset._hasMixedContent).toBe(true); @@ -2611,12 +2611,12 @@ defineSuite([ // 2 for root tile, 1 for child, 1 for stencil clear expect(statistics.numberOfCommands).toEqual(1); - expect(root.selected).toBe(true); + expect(isSelected(tileset, root)).toBe(true); expect(root._finalResolution).toBe(true); - expect(root.children[0].children[0].children[0].selected).toBe(false); - expect(root.children[0].children[0].children[1].selected).toBe(false); - expect(root.children[0].children[0].children[2].selected).toBe(false); - expect(root.children[0].children[0].children[3].selected).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[0])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[1])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[2])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[3])).toBe(false); expect(tileset._hasMixedContent).toBe(false); }); }); @@ -2650,8 +2650,7 @@ defineSuite([ it('loadSiblings', function() { viewBottomLeft(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url, { - loadSiblings : false, - baseScreenSpaceError: 1000000000 + loadSiblings : false }).then(function(tileset) { var statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toBe(2); @@ -2663,8 +2662,8 @@ defineSuite([ }); }); - xit('immediatelyLoadDesiredLevelOfDetail', function() { - viewBottomRight(); + it('immediatelyLoadDesiredLevelOfDetail', function() { + viewBottomLeft(); var tileset = scene.primitives.add(new Cesium3DTileset({ url : tilesetOfTilesetsUrl, immediatelyLoadDesiredLevelOfDetail : true @@ -2674,9 +2673,19 @@ defineSuite([ return tileset._root.contentReadyPromise.then(function() { tileset._root.refine = Cesium3DTileRefine.REPLACE; tileset._root.children[0].refine = Cesium3DTileRefine.REPLACE; + tileset._allTilesAdditive = false; return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { var statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toBe(1); + // Renders child while parent loads + viewRootOnly(); + scene.renderForSpecs(); + expect(isSelected(tileset, tileset._root.children[0])); + expect(!isSelected(tileset, tileset._root)); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(!isSelected(tileset, tileset._root.children[0])); + expect(isSelected(tileset, tileset._root)); + }); }); }); }); @@ -2695,8 +2704,8 @@ defineSuite([ expect(child.contentReady).toBe(true); expect(parent.contentReady).toBe(false); - expect(child.selected).toBe(true); - expect(parent.selected).toBe(false); + expect(isSelected(tileset, child)).toBe(true); + expect(isSelected(tileset, parent)).toBe(false); expect(statistics.numberOfCommands).toEqual(1); }); }); @@ -2883,7 +2892,11 @@ defineSuite([ tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); // Stays in the expired state until the request goes through + var originalMaxmimumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through scene.renderForSpecs(); + RequestScheduler.maximumRequests = originalMaxmimumRequests; + expect(tile.contentExpired).toBe(true); return pollToPromise(function() {