From 078e88a71a417a667034d2f50f400e79541edc0e Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Thu, 31 Mar 2016 08:40:32 -0400 Subject: [PATCH 01/22] Added number of ready tiles to stats --- Source/Scene/Cesium3DTileset.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 62475a600cf7..b4bfab8de70a 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -145,12 +145,14 @@ define([ // Loading stats numberOfPendingRequests : 0, numberProcessing : 0, + numberReady : 0, // Number of tiles with content loaded lastSelected : -1, lastVisited : -1, lastNumberOfCommands : -1, lastNumberOfPendingRequests : -1, - lastNumberProcessing : -1 + lastNumberProcessing : -1, + lastNumberReady : -1 }; /** @@ -883,6 +885,7 @@ define([ // Remove from processing queue tileset._processingQueue.splice(index, 1); --tileset._statistics.numberProcessing; + ++tileset._statistics.numberReady; } else { // Not in processing queue // For example, when a url request fails and the ready promise is rejected @@ -924,6 +927,7 @@ define([ stats.lastSelected !== tileset._selectedTiles.length || stats.lastNumberOfPendingRequests !== stats.numberOfPendingRequests || stats.lastNumberProcessing !== stats.numberProcessing || + stats.lastNumberReady !== stats.numberReady || styleStats.lastNumberOfTilesStyled !== styleStats.numberOfTilesStyled || styleStats.lastNumberOfFeaturesStyled !== styleStats.numberOfFeaturesStyled)) { @@ -940,6 +944,7 @@ define([ ', Commands: ' + stats.numberOfCommands + ', Requests: ' + stats.numberOfPendingRequests + ', Processing: ' + stats.numberProcessing + + ', Ready: ' + stats.numberReady + ', Tiles styled: ' + styleStats.numberOfTilesStyled + ', Features styled: ' + styleStats.numberOfFeaturesStyled; @@ -952,6 +957,7 @@ define([ stats.lastSelected = tileset._selectedTiles.length; stats.lastNumberOfPendingRequests = stats.numberOfPendingRequests; stats.lastNumberProcessing = stats.numberProcessing; + stats.lastNumberReady = stats.numberReady; styleStats.lastNumberOfTilesStyled = styleStats.numberOfTilesStyled; styleStats.lastNumberOfFeaturesStyled = styleStats.numberOfFeaturesStyled; } From af7b359e9806652273970284d480ec0cf01b8882 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Thu, 31 Mar 2016 09:19:01 -0400 Subject: [PATCH 02/22] Add total tiles debug stat --- Source/Scene/Cesium3DTileset.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index b4bfab8de70a..e7aafb80856a 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -146,13 +146,15 @@ define([ numberOfPendingRequests : 0, numberProcessing : 0, numberReady : 0, // Number of tiles with content loaded + numberTotal : 0, // Number of tiles in tileset.json (and other tileset.json files as they are loaded) lastSelected : -1, lastVisited : -1, lastNumberOfCommands : -1, lastNumberOfPendingRequests : -1, lastNumberProcessing : -1, - lastNumberReady : -1 + lastNumberReady : -1, + lastNumberTotal : -1 }; /** @@ -500,6 +502,8 @@ define([ throw new DeveloperError('The tileset must be 3D Tiles version 0.0. See https://github.com/AnalyticalGraphicsInc/3d-tiles#spec-status'); } + var stats = that._statistics; + // Append the version to the baseUrl var versionQuery = '?v=' + defaultValue(tilesetJson.asset.tilesetVersion, '0.0'); that._baseUrl = joinUrls(that._baseUrl, versionQuery); @@ -516,6 +520,7 @@ define([ parentTile.children.push(rootTile); ++parentTile.numberOfChildrenWithoutContent; } + ++stats.numberTotal; var refiningTiles = []; @@ -536,6 +541,7 @@ define([ var childHeader = children[k]; var childTile = new Cesium3DTile(that, baseUrl, childHeader, tile3D); tile3D.children.push(childTile); + ++stats.numberTotal; stack.push({ header : childHeader, cesium3DTile : childTile @@ -928,6 +934,7 @@ define([ stats.lastNumberOfPendingRequests !== stats.numberOfPendingRequests || stats.lastNumberProcessing !== stats.numberProcessing || stats.lastNumberReady !== stats.numberReady || + stats.lastNumberTotal !== stats.numberTotal || styleStats.lastNumberOfTilesStyled !== styleStats.numberOfTilesStyled || styleStats.lastNumberOfFeaturesStyled !== styleStats.numberOfFeaturesStyled)) { @@ -945,6 +952,9 @@ define([ ', Requests: ' + stats.numberOfPendingRequests + ', Processing: ' + stats.numberProcessing + ', Ready: ' + stats.numberReady + + // Total number of tiles includes tiles without content, so "Ready" may never reach + // "Total." Total also will increase when a tile with a tileset.json content is loaded. + ', Total: ' + stats.numberTotal + ', Tiles styled: ' + styleStats.numberOfTilesStyled + ', Features styled: ' + styleStats.numberOfFeaturesStyled; @@ -958,6 +968,7 @@ define([ stats.lastNumberOfPendingRequests = stats.numberOfPendingRequests; stats.lastNumberProcessing = stats.numberProcessing; stats.lastNumberReady = stats.numberReady; + stats.lastNumberTotal = stats.numberTotal; styleStats.lastNumberOfTilesStyled = styleStats.numberOfTilesStyled; styleStats.lastNumberOfFeaturesStyled = styleStats.numberOfFeaturesStyled; } From 6dfa3bb5bc4ac47678c3447c9c480877ac1db114 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Thu, 31 Mar 2016 10:07:52 -0400 Subject: [PATCH 03/22] Separate color and pick stats --- Source/Scene/Cesium3DTileStyleEngine.js | 22 ++---- Source/Scene/Cesium3DTileset.js | 99 ++++++++++++++++--------- Specs/Scene/Cesium3DTilesetSpec.js | 16 ++++ 3 files changed, 86 insertions(+), 51 deletions(-) diff --git a/Source/Scene/Cesium3DTileStyleEngine.js b/Source/Scene/Cesium3DTileStyleEngine.js index e205b68e35e7..7d00c0723cf3 100644 --- a/Source/Scene/Cesium3DTileStyleEngine.js +++ b/Source/Scene/Cesium3DTileStyleEngine.js @@ -16,14 +16,6 @@ define([ this._style = undefined; // The style provided by the user this._styleDirty = false; // true when the style is reassigned this._lastStyleTime = 0; // The "time" when the last style was assigned - - this.statistics = { - numberOfTilesStyled : 0, - numberOfFeaturesStyled : 0, - - lastNumberOfTilesStyled : -1, - lastNumberOfFeaturesStyled : -1 - }; } defineProperties(Cesium3DTileStyleEngine.prototype, { @@ -64,7 +56,7 @@ define([ } var lastStyleTime = this._lastStyleTime; - var stats = this.statistics; + var stats = tileset._statistics; // If a new style was assigned, loop through all the visible tiles; otherwise, loop through // only the tiles that are newly visible, i.e., they are visible this frame, but were not @@ -82,7 +74,7 @@ define([ // 2) this tile is now visible, but it wasn't visible when the style was first assigned if (tile.lastStyleTime !== lastStyleTime) { tile.lastStyleTime = lastStyleTime; - styleCompositeContent(this, tile.content); + styleCompositeContent(this, tile.content, stats); ++stats.numberOfTilesStyled; } @@ -90,27 +82,27 @@ define([ } }; - function styleCompositeContent(styleEngine, content) { + function styleCompositeContent(styleEngine, content, stats) { var innerContents = content.innerContents; if (defined(innerContents)) { var length = innerContents.length; for (var i = 0; i < length; ++i) { // Recurse for composites of composites - styleCompositeContent(styleEngine, innerContents[i]); + styleCompositeContent(styleEngine, innerContents[i], stats); } } else { // Not a composite tile - styleContent(styleEngine, content); + styleContent(styleEngine, content, stats); } } var scratchColor = new Color(); - function styleContent(styleEngine, content) { + function styleContent(styleEngine, content, stats) { var length = content.featuresLength; var style = styleEngine._style; - styleEngine.statistics.numberOfFeaturesStyled += length; + stats.numberOfFeaturesStyled += length; if (!defined(style)) { clearStyle(content); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e7aafb80856a..393ded806a5d 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -61,6 +61,7 @@ define([ * @param {Boolean} [options.show=true] Determines if the tileset will be shown. * @param {Number} [options.maximumScreenSpaceError=16] The maximum screen-space error used to drive level-of-detail refinement. * @param {Boolean} [options.debugShowStatistics=false] For debugging only. Determines if rendering statistics are output to the console. + * @param {Boolean} [options.debugShowPickStatistics=false] For debugging only. Determines if rendering statistics for picking are output to the console. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. When true, renders the bounding volume for each tile. @@ -138,6 +139,18 @@ define([ * @default false */ this.debugShowStatistics = defaultValue(options.debugShowStatistics, false); + + /** + * This property is for debugging only; it is not optimized for production use. + *

+ * Determines if rendering statistics for picking are output to the console. + *

+ * + * @type {Boolean} + * @default false + */ + this.debugShowPickStatistics = defaultValue(options.debugShowPickStatistics, false); + this._statistics = { // Rendering stats visited : 0, @@ -147,14 +160,12 @@ define([ numberProcessing : 0, numberReady : 0, // Number of tiles with content loaded numberTotal : 0, // Number of tiles in tileset.json (and other tileset.json files as they are loaded) + // Styling stats + numberOfTilesStyled : 0, + numberOfFeaturesStyled : 0, - lastSelected : -1, - lastVisited : -1, - lastNumberOfCommands : -1, - lastNumberOfPendingRequests : -1, - lastNumberProcessing : -1, - lastNumberReady : -1, - lastNumberTotal : -1 + lastColor : new Cesium3DTilesetStatistics(), + lastPick : new Cesium3DTilesetStatistics() }; /** @@ -282,6 +293,18 @@ define([ }); } + function Cesium3DTilesetStatistics() { + this.selected = -1; + this.visited = -1; + this.numberOfCommands = -1; + this.numberOfPendingRequests = -1; + this.numberProcessing = -1; + this.numberReady = -1; + this.numberTotal = -1; + this.numberOfTilesStyled = -1; + this.numberOfFeaturesStyled = -1; + }; + defineProperties(Cesium3DTileset.prototype, { /** * Gets the tileset's asset object property, which contains metadata about the tileset. @@ -917,31 +940,41 @@ define([ var stats = tileset._statistics; stats.visited = 0; stats.numberOfCommands = 0; - - var styleStats = tileset._styleEngine.statistics; - styleStats.numberOfTilesStyled = 0; - styleStats.numberOfFeaturesStyled = 0; + stats.numberOfTilesStyled = 0; + stats.numberOfFeaturesStyled = 0; } function showStats(tileset, isPick) { var stats = tileset._statistics; - var styleStats = tileset._styleEngine.statistics; - - if (tileset.debugShowStatistics && ( - stats.lastVisited !== stats.visited || - stats.lastNumberOfCommands !== stats.numberOfCommands || - stats.lastSelected !== tileset._selectedTiles.length || - stats.lastNumberOfPendingRequests !== stats.numberOfPendingRequests || - stats.lastNumberProcessing !== stats.numberProcessing || - stats.lastNumberReady !== stats.numberReady || - stats.lastNumberTotal !== stats.numberTotal || - styleStats.lastNumberOfTilesStyled !== styleStats.numberOfTilesStyled || - styleStats.lastNumberOfFeaturesStyled !== styleStats.numberOfFeaturesStyled)) { + var last = isPick ? stats.lastPick : stats.lastColor; + + if (((tileset.debugShowStatistics && !isPick) || + (tileset.debugShowPickStatistics && isPick)) && + (last.visited !== stats.visited || + last.numberOfCommands !== stats.numberOfCommands || + last.selected !== tileset._selectedTiles.length || + last.numberOfPendingRequests !== stats.numberOfPendingRequests || + last.numberProcessing !== stats.numberProcessing || + last.numberReady !== stats.numberReady || + last.numberTotal !== stats.numberTotal || + last.numberOfTilesStyled !== stats.numberOfTilesStyled || + last.numberOfFeaturesStyled !== stats.numberOfFeaturesStyled)) { + + last.visited = stats.visited; + last.numberOfCommands = stats.numberOfCommands; + last.selected = tileset._selectedTiles.length; + last.numberOfPendingRequests = stats.numberOfPendingRequests; + last.numberProcessing = stats.numberProcessing; + last.numberReady = stats.numberReady; + last.numberTotal = stats.numberTotal; + last.numberOfTilesStyled = stats.numberOfTilesStyled; + last.numberOfFeaturesStyled = stats.numberOfFeaturesStyled; // Since the pick pass uses a smaller frustum around the pixel of interest, // the stats will be different than the normal render pass. var s = isPick ? '[Pick ]: ' : '[Color]: '; s += + // --- Rendering stats 'Visited: ' + stats.visited + // Number of commands returned is likely to be higher than the number of tiles selected // because of tiles that create multiple commands. @@ -949,28 +982,22 @@ define([ // Number of commands executed is likely to be higher because of commands overlapping // multiple frustums. ', Commands: ' + stats.numberOfCommands + - ', Requests: ' + stats.numberOfPendingRequests + + + // --- Cache/loading stats + ' | Requests: ' + stats.numberOfPendingRequests + ', Processing: ' + stats.numberProcessing + ', Ready: ' + stats.numberReady + // Total number of tiles includes tiles without content, so "Ready" may never reach // "Total." Total also will increase when a tile with a tileset.json content is loaded. ', Total: ' + stats.numberTotal + - ', Tiles styled: ' + styleStats.numberOfTilesStyled + - ', Features styled: ' + styleStats.numberOfFeaturesStyled; + + // --- Styling stats + ' | Tiles styled: ' + stats.numberOfTilesStyled + + ', Features styled: ' + stats.numberOfFeaturesStyled; /*global console*/ console.log(s); } - - stats.lastVisited = stats.visited; - stats.lastNumberOfCommands = stats.numberOfCommands; - stats.lastSelected = tileset._selectedTiles.length; - stats.lastNumberOfPendingRequests = stats.numberOfPendingRequests; - stats.lastNumberProcessing = stats.numberProcessing; - stats.lastNumberReady = stats.numberReady; - stats.lastNumberTotal = stats.numberTotal; - styleStats.lastNumberOfTilesStyled = styleStats.numberOfTilesStyled; - styleStats.lastNumberOfFeaturesStyled = styleStats.numberOfFeaturesStyled; } function updateTiles(tileset, frameState) { diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index f0aa5b5da958..58bf9a694ea4 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -677,12 +677,28 @@ defineSuite([ spyOn(console, 'log'); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + scene.renderForSpecs(); + expect(console.log).not.toHaveBeenCalled(); + tileset.debugShowStatistics = true; scene.renderForSpecs(); expect(console.log).toHaveBeenCalled(); }); }); + it('debugShowPickStatistics', function() { + spyOn(console, 'log'); + + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + scene.pickForSpecs(); + expect(console.log).not.toHaveBeenCalled(); + + tileset.debugShowPickStatistics = true; + scene.pickForSpecs(); + expect(console.log).toHaveBeenCalled(); + }); + }); + it('debugColorizeTiles', function() { // More precise test is in Cesium3DTileBatchTableResourcesSpec return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { From cd4cca67f62e65a35330d2c90981e197879a0889 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Thu, 31 Mar 2016 11:44:59 -0400 Subject: [PATCH 04/22] Fix test --- Source/Scene/Cesium3DTileset.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 393ded806a5d..cffd446c9537 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1025,10 +1025,11 @@ define([ /////////////////////////////////////////////////////////////////////////// function raiseLoadProgressEvent(tileset, frameState) { - var numberOfPendingRequests = tileset._statistics.numberOfPendingRequests; - var numberProcessing = tileset._statistics.numberProcessing; - var lastNumberOfPendingRequest = tileset._statistics.lastNumberOfPendingRequests; - var lastNumberProcessing = tileset._statistics.lastNumberProcessing; + var stats = tileset._statistics; + var numberOfPendingRequests = stats.numberOfPendingRequests; + var numberProcessing = stats.numberProcessing; + var lastNumberOfPendingRequest = stats.lastColor.numberOfPendingRequests; + var lastNumberProcessing = stats.lastColor.numberProcessing; if ((numberOfPendingRequests !== lastNumberOfPendingRequest) || (numberProcessing !== lastNumberProcessing)) { frameState.afterRender.push(function() { From 6c10764178405d55a5f08dcbe5f12e1fa7428e0a Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Mon, 4 Apr 2016 15:33:54 -0400 Subject: [PATCH 05/22] Fix Cesium3DTileBatchTableResources tests --- Source/Scene/Cesium3DTileBatchTableResources.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Cesium3DTileBatchTableResources.js b/Source/Scene/Cesium3DTileBatchTableResources.js index 7dab3520360b..fb84458e8d91 100644 --- a/Source/Scene/Cesium3DTileBatchTableResources.js +++ b/Source/Scene/Cesium3DTileBatchTableResources.js @@ -506,14 +506,14 @@ define([ ' { \n' + ' if (!isStyleTranslucent && !tile_translucentCommand) \n' + // Do not render opaque features in the translucent pass ' { \n' + - ' gl_Position *= 0.0; \n' + + ' discard; \n' + ' } \n' + ' } \n' + ' else \n' + ' { \n' + ' if (isStyleTranslucent) \n' + // Do not render translucent features in the opaque pass ' { \n' + - ' gl_Position *= 0.0; \n' + + ' discard; \n' + ' } \n' + ' } \n' + ' tile_main(); \n' + From 831ae5e8245a9f14105070d597dc15265e680b49 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Mon, 4 Apr 2016 16:17:20 -0400 Subject: [PATCH 06/22] Add missing interface function --- Source/Scene/Empty3DTileContent.js | 6 ++++++ Source/Scene/Tileset3DTileContent.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Source/Scene/Empty3DTileContent.js b/Source/Scene/Empty3DTileContent.js index 11f067e2a950..b5f936ff5eb2 100644 --- a/Source/Scene/Empty3DTileContent.js +++ b/Source/Scene/Empty3DTileContent.js @@ -82,6 +82,12 @@ define([ Empty3DTileContent.prototype.request = function() { }; + /** + * Part of the {@link Cesium3DTileContent} interface. + */ + Empty3DTileContent.prototype.initialize = function(arrayBuffer, byteOffset) { + }; + /** * Part of the {@link Cesium3DTileContent} interface. */ diff --git a/Source/Scene/Tileset3DTileContent.js b/Source/Scene/Tileset3DTileContent.js index 4099ed1d17b1..933735efd1c7 100644 --- a/Source/Scene/Tileset3DTileContent.js +++ b/Source/Scene/Tileset3DTileContent.js @@ -92,6 +92,12 @@ define([ }); }; + /** + * Part of the {@link Cesium3DTileContent} interface. + */ + Tileset3DTileContent.prototype.initialize = function(arrayBuffer, byteOffset) { + }; + /** * Part of the {@link Cesium3DTileContent} interface. */ From cb6719c258244e07d13cd0a5c5df779546b3d67d Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 09:47:36 -0400 Subject: [PATCH 07/22] Add doubly linked list --- Source/Core/DoublyLinkedList.js | 111 +++++++++ Specs/Core/DoublyLinkedListSpec.js | 381 +++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 Source/Core/DoublyLinkedList.js create mode 100644 Specs/Core/DoublyLinkedListSpec.js diff --git a/Source/Core/DoublyLinkedList.js b/Source/Core/DoublyLinkedList.js new file mode 100644 index 000000000000..02dd8d86ffbd --- /dev/null +++ b/Source/Core/DoublyLinkedList.js @@ -0,0 +1,111 @@ +/*global define*/ +define([ + '../Core/defined', + '../Core/defineProperties', + '../Core/DeveloperError' + ], function( + defined, + defineProperties, + DeveloperError) { + 'use strict'; + + /** + * @private + */ + function DoublyLinkedList() { + this.head = undefined; + this.tail = undefined; + this._length = 0; + } + + defineProperties(DoublyLinkedList.prototype, { + length : { + get : function() { + return this._length; + } + } + }); + + function DoublyLinkedListNode(item, previous, next) { + this.item = item; + this.previous = previous; + this.next = next; + } + + DoublyLinkedList.prototype.add = function(item) { + var node = new DoublyLinkedListNode(item, this.tail, undefined); + + if (defined(this.tail)) { + this.tail.next = node; + this.tail = node; + } else { + // Insert into empty linked list + this.head = node; + this.tail = node; + } + + ++this._length; + + return node; + }; + + function remove(list, node) { + if (defined(node.previous) && defined(node.next)) { + node.previous.next = node.next; + node.next.previous = node.previous; + } else if (defined(node.previous)) { + // Remove last node + node.previous.next = undefined; + list.tail = node.previous; + } else if (defined(node.next)) { + // Remove first node + node.next.previous = undefined; + list.head = node.next; + } else { + // Remove last node in the linked list + list.head = undefined; + list.tail = undefined; + } + + node.next = undefined; + node.previous = undefined; + } + + DoublyLinkedList.prototype.remove = function(node) { + if (!defined(node)) { + return; + } + + remove(this, node); + + --this._length; + }; + + DoublyLinkedList.prototype.splice = function(node, nextNode) { + if (!defined(node) || !defined(nextNode)) { + throw new DeveloperError('node and nextNode are required.'); + } + + if (node === nextNode) { + return; + } + + // Remove nextNode, then insert after node + remove(this, nextNode); + + var oldNodeNext = node.next; + node.next = nextNode; + + // nextNode is the new tail + if (this.tail === node) { + this.tail = nextNode; + } else { + oldNodeNext.previous = nextNode; + } + + nextNode.next = oldNodeNext; + nextNode.previous = node; + } + + return DoublyLinkedList; +}); diff --git a/Specs/Core/DoublyLinkedListSpec.js b/Specs/Core/DoublyLinkedListSpec.js new file mode 100644 index 000000000000..027ea189b5f8 --- /dev/null +++ b/Specs/Core/DoublyLinkedListSpec.js @@ -0,0 +1,381 @@ +/*global defineSuite*/ +fdefineSuite([ + 'Core/DoublyLinkedList' + ], function( + DoublyLinkedList) { + 'use strict'; + + it('constructs', function() { + var list = new DoublyLinkedList(); + expect(list.head).not.toBeDefined(); + expect(list.tail).not.toBeDefined(); + expect(list.length).toEqual(0); + }); + + it('adds items', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + // node + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + + expect(node).toBeDefined(); + expect(node.item).toEqual(1); + expect(node.previous).not.toBeDefined(); + expect(node.next).not.toBeDefined(); + + var node2 = list.add(2); + + // node <-> node2 + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node2); + expect(list.length).toEqual(2); + + expect(node2).toBeDefined(); + expect(node2.item).toEqual(2); + expect(node2.previous).toEqual(node); + expect(node2.next).not.toBeDefined(); + + expect(node.next).toEqual(node2); + + var node3 = list.add(3); + + // node <-> node2 <-> node3 + // ^ ^ + // | | + // head tail + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node3); + expect(list.length).toEqual(3); + + expect(node3).toBeDefined(); + expect(node3.item).toEqual(3); + expect(node3.previous).toEqual(node2); + expect(node3.next).not.toBeDefined(); + + expect(node2.next).toEqual(node3); + }); + + it('removes from a list with one item', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + list.remove(node); + + expect(list.head).not.toBeDefined(); + expect(list.tail).not.toBeDefined(); + expect(list.length).toEqual(0); + }); + + it('removes head of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + + list.remove(node); + + expect(list.head).toEqual(node2); + expect(list.tail).toEqual(node2); + expect(list.length).toEqual(1); + }); + + it('removes tail of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + + list.remove(node2); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + }); + + it('removes middle of list', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(2); + + list.remove(node2); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node3); + expect(list.length).toEqual(2); + }); + + it('removes nothing', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + list.remove(undefined); + + expect(list.head).toEqual(node); + expect(list.tail).toEqual(node); + expect(list.length).toEqual(1); + }); + + function expectOrder(list, nodes) { + // Assumes at least one node is in the list + var length = nodes.length; + + expect(list.length).toEqual(length); + + // Verify head and tail pointers + expect(list.head).toEqual(nodes[0]); + expect(list.tail).toEqual(nodes[length - 1]); + + // Verify that linked list has nodes in the expected order + var node = list.head; + for (var i = 0; i < length; ++i) { + var nextNode = (i === length - 1) ? undefined : nodes[i + 1]; + var previousNode = (i === 0) ? undefined : nodes[i - 1]; + + expect(node).toEqual(nodes[i]); + expect(node.next).toEqual(nextNode); + expect(node.previous).toEqual(previousNode); + + node = node.next; + } + } + + it('splices nextNode before node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + var node5 = list.add(5); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 <-> node5 + // ^ ^ ^ ^ + // | | | | + // head nextNode node tail + + // After: + // + // node <-> node3 <-> node4 <-> node2 <-> node5 + // ^ ^ + // | | + // head tail + + // Move node2 after node4 + list.splice(node4, node2); + expectOrder(list, [node, node3, node4, node2, node5]); + }); + + it('splices nextNode after node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + var node5 = list.add(5); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 <-> node5 + // ^ ^ ^ ^ + // | | | | + // head node nextNode tail + + // After: + // + // node <-> node2 <-> node4 <-> node3 <-> node5 + // ^ ^ + // | | + // head tail + + // Move node4 after node2 + list.splice(node2, node4); + expectOrder(list, [node, node2, node4, node3, node5]); + }); + + it('splices nextNode immediately before node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ ^ + // | | | | + // head nextNode node tail + + // After: + // + // node <-> node3 <-> node2 <-> node4 + // ^ ^ + // | | + // head tail + + // Move node2 after node4 + list.splice(node3, node2); + expectOrder(list, [node, node3, node2, node4]); + }); + + it('splices nextNode immediately after node', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ ^ + // | | | | + // head node nextNode tail + + // After: does not change + + list.splice(node2, node3); + expectOrder(list, [node, node2, node3, node4]); + }); + + it('splices node === nextNode', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + + // Before: + // + // node <-> node2 <-> node3 + // ^ ^ ^ + // | | | + // head node/nextNode tail + + // After: does not change + + list.splice(node2, node2); + expectOrder(list, [node, node2, node3]); + }); + + it('splices when nextNode was tail', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head node tail/nextNode + + // After: + // + // node <-> node2 <-> node4 <-> node3 + // ^ ^ + // | | + // head tail + + list.splice(node2, node4); + expectOrder(list, [node, node2, node4, node3]); + }); + + it('splices when node was tail', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head nextNode tail/node + + // After: + // + // node <-> node3 <-> node4 <-> node2 + // ^ ^ + // | | + // head tail/node + + list.splice(node4, node2); + expectOrder(list, [node, node3, node4, node2]); + }); + + it('splices when nextNode was head', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head/nextNode node tail + + // After: + // + // node2 <-> node3 <-> node <-> node4 + // ^ ^ + // | | + // head tail + + list.splice(node3, node); + expectOrder(list, [node2, node3, node, node4]); + }); + + it('splices when node was head', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + var node2 = list.add(2); + var node3 = list.add(3); + var node4 = list.add(4); + + // Before: + // + // node <-> node2 <-> node3 <-> node4 + // ^ ^ ^ + // | | | + // head/node nextNode tail + + // After: + // + // node <-> node3 <-> node2 <-> node4 + // ^ ^ + // | | + // head tail + + list.splice(node, node3); + expectOrder(list, [node, node3, node2, node4]); + }); + + it('splice throws without nodes', function() { + var list = new DoublyLinkedList(); + var node = list.add(1); + + expect(function() { + list.splice(undefined, node); + }).toThrowDeveloperError(); + + expect(function() { + list.splice(node, undefined); + }).toThrowDeveloperError(); + }); +}); From 8c47fe0e6c6fa3b77f56020eb11f5c4fb1ded253 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 09:48:09 -0400 Subject: [PATCH 08/22] Remove fdefineSuite --- Specs/Core/DoublyLinkedListSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Specs/Core/DoublyLinkedListSpec.js b/Specs/Core/DoublyLinkedListSpec.js index 027ea189b5f8..e66cd339f3da 100644 --- a/Specs/Core/DoublyLinkedListSpec.js +++ b/Specs/Core/DoublyLinkedListSpec.js @@ -1,5 +1,5 @@ /*global defineSuite*/ -fdefineSuite([ +defineSuite([ 'Core/DoublyLinkedList' ], function( DoublyLinkedList) { From 1fd0a2afc9c1406ef8ac2249b70559a3080fdd2c Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 12:17:37 -0400 Subject: [PATCH 09/22] Start of cache replacement for 3D Tiles --- Source/Scene/Cesium3DTile.js | 123 ++++++++++++++++----------- Source/Scene/Cesium3DTileset.js | 130 +++++++++++++++++++++++++---- Specs/Scene/Cesium3DTilesetSpec.js | 23 ++--- 3 files changed, 193 insertions(+), 83 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 550dddf3312d..40ba4b1c7134 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -166,20 +166,10 @@ define([ */ this.numberOfChildrenWithoutContent = defined(header.children) ? header.children.length : 0; - /** - * Gets the promise that will be resolved when the tile's content is ready to render. - * - * @type {Promise.} - * @readonly - * - * @private - */ - this.contentReadyPromise = when.defer(); - - var content; var hasContent; var hasTilesetContent; var requestServer; + var createContent; if (defined(contentHeader)) { var contentUrl = contentHeader.url; @@ -202,14 +192,23 @@ define([ } //>>includeEnd('debug'); - content = contentFactory(tileset, this, url); + var that = this; + createContent = function() { + return contentFactory(tileset, that, url); + }; } else { - content = new Empty3DTileContent(); hasContent = false; hasTilesetContent = false; + + createContent = function() { + return new Empty3DTileContent(); + }; } - this._content = content; + this._createContent = createContent; + this._content = createContent(); + addContentReadyPromise(this); + this._requestServer = requestServer; /** @@ -235,21 +234,12 @@ define([ */ this.hasTilesetContent = hasTilesetContent; - var that = this; - - // Content enters the READY state - when(content.readyPromise).then(function(content) { - if (defined(that.parent)) { - --that.parent.numberOfChildrenWithoutContent; - } - - that.contentReadyPromise.resolve(that); - }).otherwise(function(error) { - // In this case, that.parent.numberOfChildrenWithoutContent will never reach zero - // and therefore that.parent will never refine. If this becomes an issue, failed - // requests can be reissued. - that.contentReadyPromise.reject(error); - }); + /** + * DOC_TBA + * + * @private + */ + this.replacementNode = undefined; // Members that are updated every frame for tree traversal and rendering optimizations: @@ -281,13 +271,24 @@ define([ this.selected = false; /** - * The last frame number the tile was visible in. + * The last frame number the tile was selected in. * * @type {Number} * * @private */ - this.lastFrameNumber = 0; + this.lastSelectedFrameNumber = 0; + + /** + * The last frame number the tile was visited during tile selection. A tile may be touched, + * but not selected because, for example, it is a parent using replacement refinement and + * its children are selected. A selected tile will always be touched. + * + * @type {Number} + * + * @private + */ + this.lastTouchedFrameNumber = 0; /** * The time when a style was last applied to this tile. @@ -336,24 +337,6 @@ define([ } }, - /** - * 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 - * to render. - * - * @memberof Cesium3DTile.prototype - * - * @type {Promise.} - * @readonly - * - * @private - */ - contentReadyToProcessPromise : { - get : function() { - return this._content.contentReadyToProcessPromise; - } - }, - /** * @readonly * @private @@ -395,6 +378,19 @@ define([ } }); + function addContentReadyPromise(tile) { + // Content enters the READY state + when(tile._content.readyPromise).then(function(content) { + if (defined(tile.parent)) { + --tile.parent.numberOfChildrenWithoutContent; + } + }).otherwise(function(error) { + // In this case, that.parent.numberOfChildrenWithoutContent will never reach zero + // and therefore that.parent will never refine. If this becomes an issue, failed + // requests can be reissued. + }); + } + /** * Requests the tile's content. *

@@ -423,6 +419,35 @@ define([ return this._requestServer.hasAvailableRequests(); }; + /** + * Unloads the tile's content and returns the tile's state to the state of when + * it was first created, before its content were loaded. + * + * @private + */ + Cesium3DTile.prototype.unloadContent = function() { + if (defined(this.parent)) { + ++this.parent.numberOfChildrenWithoutContent; + } + + this._content = this._content && this._content.destroy(); + this._content = this._createContent(); + addContentReadyPromise(this); + + this.replacementNode = undefined; + + // Restore properties set per frame to their defaults + this.distanceToCamera = 0; + this.parentPlaneMask = 0; + this.selected = false; + this.lastSelectedFrameNumber = 0; + this.lastTouchedFrameNumber = 0; + this.lastStyleTime = 0; + + this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); + this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); + }; + /** * Determines whether the tile's bounding volume intersects the culling volume. * diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index cffd446c9537..ca48c971a807 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -5,6 +5,7 @@ define([ '../Core/defineProperties', '../Core/destroyObject', '../Core/DeveloperError', + '../Core/DoublyLinkedList', '../Core/Event', '../Core/getBaseUri', '../Core/getExtensionFromUri', @@ -29,6 +30,7 @@ define([ defineProperties, destroyObject, DeveloperError, + DoublyLinkedList, Event, getBaseUri, getExtensionFromUri, @@ -49,6 +51,15 @@ define([ SceneMode) { 'use strict'; +// TODO: unload events +// TODO: unit tests +// TODO: test with replacement refinement +// TODO: Refactor TileReplacementQueue to use DoublyLinkedList? +// TODO: track stats for cache trashing +// TODO: good default for maximumNumberOfLoadedTiles +// TODO: More precise size than number of tiles? Count composite tiles as more? Include geometry/texture cost with each tile? +// TODO: unload sub-trees from tiles with tileset.json content + /** * A {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles tileset}, * used for streaming massive heterogeneous 3D geospatial datasets. @@ -110,6 +121,13 @@ define([ this._selectedTiles = []; this._selectedTilesToStyle = []; + 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(); + /** * Determines if the tileset will be shown. * @@ -127,6 +145,22 @@ define([ */ this.maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); + /** + * The maximum number of tiles to load. Tiles not in view are unloaded to enforce this. + *

+ * If more tiles than maximumNumberOfLoadedTiles are needed + * to meet the desired screen-space error, determined by {@link Cesium3DTileset#maximumScreenSpaceError}, + * for the current view than the number of tiles loaded will exceed + * maximumNumberOfLoadedTiles. For example, if the maximum is 128 tiles, but + * 150 tiles are needed to meet the screen-space error, then 150 tiles may be loaded. When + * these tiles go out of view, they will be unloaded. + *

+ * + * @type {Number} + * @default 256 + */ + this.maximumNumberOfLoadedTiles = defaultValue(options.maximumNumberOfLoadedTiles, 256); + this._styleEngine = new Cesium3DTileStyleEngine(); /** @@ -139,6 +173,7 @@ define([ * @default false */ this.debugShowStatistics = defaultValue(options.debugShowStatistics, false); + this._debugShowStatistics = false; /** * This property is for debugging only; it is not optimized for production use. @@ -150,6 +185,7 @@ define([ * @default false */ this.debugShowPickStatistics = defaultValue(options.debugShowPickStatistics, false); + this._debugShowPickStatistics = false; this._statistics = { // Rendering stats @@ -660,8 +696,8 @@ define([ ++stats.numberOfPendingRequests; var removeFunction = removeFromProcessingQueue(tileset, tile); - when(tile.contentReadyToProcessPromise).then(addToProcessingQueue(tileset, tile)).otherwise(removeFunction); - when(tile.contentReadyPromise).then(removeFunction).otherwise(removeFunction); + when(tile.content.contentReadyToProcessPromise).then(addToProcessingQueue(tileset, tile)).otherwise(removeFunction); + when(tile.content.readyPromise).then(removeFunction).otherwise(removeFunction); } } @@ -678,11 +714,19 @@ define([ tileContent.featurePropertiesDirty = false; tile.lastStyleTime = 0; // Force applying the style to this tile tileset._selectedTilesToStyle.push(tile); - } else if ((tile.lastFrameNumber !== frameState.frameNumber - 1)) { - // Tile is newly visible; it is visible this frame, but was not visible last frame. + } else if ((tile.lastSelectedFrameNumber !== frameState.frameNumber - 1)) { + // Tile is newly selected; it is selected this frame, but was not selected last frame. tileset._selectedTilesToStyle.push(tile); } - tile.lastFrameNumber = frameState.frameNumber; + tile.lastSelectedFrameNumber = frameState.frameNumber; + } + } + + function touch(tileset, tile, frameState) { + var node = tile.replacementNode; + if (defined(node)) { + tile.lastTouchedFrameNumber = frameState.frameNumber; + tileset._replacementList.splice(tileset._replacementSentinel, node); } } @@ -702,6 +746,12 @@ define([ scratchRefiningTiles.length = 0; + // 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; + tileset._replacementList.splice(replacementList.tail, tileset._replacementSentinel); + var root = tileset._root; root.distanceToCamera = root.distanceToTile(frameState); root.parentPlaneMask = CullingVolume.MASK_INDETERMINATE; @@ -734,6 +784,8 @@ define([ } var fullyVisible = (planeMask === CullingVolume.MASK_INSIDE); + touch(tileset, t, frameState); + // Tile is inside/intersects the view frustum. How many pixels is its geometric error? var sse = getScreenSpaceError(t.geometricError, t, frameState); // TODO: refine also based on (1) occlusion/VMSSE and/or (2) center of viewport @@ -915,6 +967,7 @@ define([ tileset._processingQueue.splice(index, 1); --tileset._statistics.numberProcessing; ++tileset._statistics.numberReady; + tile.replacementNode = tileset._replacementList.add(tile); } else { // Not in processing queue // For example, when a url request fails and the ready promise is rejected @@ -948,8 +1001,11 @@ define([ var stats = tileset._statistics; var last = isPick ? stats.lastPick : stats.lastColor; - if (((tileset.debugShowStatistics && !isPick) || - (tileset.debugShowPickStatistics && isPick)) && + var outputStats = (tileset.debugShowStatistics && !isPick) || (tileset.debugShowPickStatistics && isPick); + var showStatsThisFrame = + ((tileset._debugShowStatistics !== tileset.debugShowStatistics) || + (tileset._debugShowPickStatistics !== tileset.debugShowPickStatistics)); + var statsChanged = (last.visited !== stats.visited || last.numberOfCommands !== stats.numberOfCommands || last.selected !== tileset._selectedTiles.length || @@ -958,17 +1014,14 @@ define([ last.numberReady !== stats.numberReady || last.numberTotal !== stats.numberTotal || last.numberOfTilesStyled !== stats.numberOfTilesStyled || - last.numberOfFeaturesStyled !== stats.numberOfFeaturesStyled)) { - - last.visited = stats.visited; - last.numberOfCommands = stats.numberOfCommands; - last.selected = tileset._selectedTiles.length; - last.numberOfPendingRequests = stats.numberOfPendingRequests; - last.numberProcessing = stats.numberProcessing; - last.numberReady = stats.numberReady; - last.numberTotal = stats.numberTotal; - last.numberOfTilesStyled = stats.numberOfTilesStyled; - last.numberOfFeaturesStyled = stats.numberOfFeaturesStyled; + last.numberOfFeaturesStyled !== stats.numberOfFeaturesStyled); + + if (outputStats && (showStatsThisFrame || statsChanged)) { + // The shadowed properties are used to ensure that when a show stats properties + // is set to true, it outputs the stats on the next frame even if they didn't + // change from the previous frame. + tileset._debugShowStatistics = tileset.debugShowStatistics; + tileset._debugShowPickStatistics = tileset.debugShowPickStatistics; // Since the pick pass uses a smaller frustum around the pixel of interest, // the stats will be different than the normal render pass. @@ -998,6 +1051,16 @@ define([ /*global console*/ console.log(s); } + + last.visited = stats.visited; + last.numberOfCommands = stats.numberOfCommands; + last.selected = tileset._selectedTiles.length; + last.numberOfPendingRequests = stats.numberOfPendingRequests; + last.numberProcessing = stats.numberProcessing; + last.numberReady = stats.numberReady; + last.numberTotal = stats.numberTotal; + last.numberOfTilesStyled = stats.numberOfTilesStyled; + last.numberOfFeaturesStyled = stats.numberOfFeaturesStyled; } function updateTiles(tileset, frameState) { @@ -1022,6 +1085,32 @@ define([ tileset._statistics.numberOfCommands = (commandList.length - numberOfInitialCommands); } + function unloadTiles(tileset, frameState) { + var stats = tileset._statistics; + var frameNumber = frameState.frameNumber; + var maximumNumberOfLoadedTiles = tileset.maximumNumberOfLoadedTiles + 1; // + 1 to account for sentinel + var replacementList = tileset._replacementList; + +// TODO: explore proactively clearing the cache - automatically or provide a function for the user. +// TODO: could check last n frames using lastTouchedFrameNumber + + // 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) && (replacementList.length > maximumNumberOfLoadedTiles)) { + node.item.unloadContent(); + + var currentNode = node; + node = node.next; + replacementList.remove(currentNode); + + --stats.numberReady; + } + } + /////////////////////////////////////////////////////////////////////////// function raiseLoadProgressEvent(tileset, frameState) { @@ -1067,9 +1156,14 @@ define([ if (outOfCore) { processTiles(this, frameState); } + selectTiles(this, frameState, outOfCore); updateTiles(this, frameState); + if (outOfCore) { + unloadTiles(this, frameState); + } + // Events are raised (added to the afterRender queue) here since promises // may resolve outside of the update loop that then raise events, e.g., // model's readyPromise. diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 58bf9a694ea4..7e4d15a1452f 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -603,7 +603,7 @@ defineSuite([ // Set view so that root's content is requested viewRootOnly(); scene.renderForSpecs(); - return root.contentReadyPromise.then(function() { + return root.content.readyPromise.then(function() { // Root has one child now, the root of the external tileset expect(root.children.length).toEqual(1); @@ -639,7 +639,7 @@ defineSuite([ viewRootOnly(); scene.renderForSpecs(); - return tileset._root.contentReadyPromise; + return tileset._root.content.readyPromise; }).then(function() { //Make sure tileset2.json was requested with query parameters and version var queryParamsWithVersion = queryParams + '&v=0.0'; @@ -760,7 +760,7 @@ defineSuite([ viewRootOnly(); scene.renderForSpecs(); // Request root expect(tileset._statistics.numberOfPendingRequests).toEqual(1); - return tileset._root.contentReadyToProcessPromise.then(function() { + return tileset._root.content.contentReadyToProcessPromise.then(function() { scene.pickForSpecs(); expect(spy).not.toHaveBeenCalled(); scene.renderForSpecs(); @@ -864,13 +864,9 @@ defineSuite([ expect(stats.numberOfPendingRequests).toEqual(1); scene.primitives.remove(tileset); - return root.contentReadyPromise.then(function(root) { - fail('should not resolve'); - }).otherwise(function(error) { - // Expect the root to not have added any children from the external tileset.json - expect(root.children.length).toEqual(0); - expect(RequestScheduler.getNumberOfAvailableRequests()).toEqual(RequestScheduler.maximumRequests); - }); + expect(root.content).not.toBeDefined(); + // Expect the root to not have added any children from the external tileset.json + expect(root.children.length).toEqual(0); }); }); @@ -884,12 +880,7 @@ defineSuite([ scene.renderForSpecs(); // Request root scene.primitives.remove(tileset); - return root.contentReadyPromise.then(function(root) { - fail('should not resolve'); - }).otherwise(function(error) { - expect(content.state).toEqual(Cesium3DTileContentState.FAILED); - expect(RequestScheduler.getNumberOfAvailableRequests()).toEqual(RequestScheduler.maximumRequests); - }); + expect(root.content).not.toBeDefined(); }); }); From aa439c1ee7df08eb3e102f4f215d2e87fb81e416 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 14:41:13 -0400 Subject: [PATCH 10/22] Added trimLoadedTiles --- Source/Scene/Cesium3DTileset.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index ca48c971a807..9d6b1123ed97 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -56,6 +56,7 @@ define([ // TODO: test with replacement refinement // TODO: Refactor TileReplacementQueue to use DoublyLinkedList? // TODO: track stats for cache trashing +// TODO: research strategies for proactive cache trimming: number of seconds/frame a tile was not selected, how far out of view a tile is, etc. // TODO: good default for maximumNumberOfLoadedTiles // TODO: More precise size than number of tiles? Count composite tiles as more? Include geometry/texture cost with each tile? // TODO: unload sub-trees from tiles with tileset.json content @@ -127,6 +128,7 @@ define([ // (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; /** * Determines if the tileset will be shown. @@ -148,6 +150,9 @@ define([ /** * The maximum number of tiles to load. Tiles not in view are unloaded to enforce this. *

+ * If decreasing this value results in unloading tiles, the tiles are unloaded the next frame. + *

+ *

* If more tiles than maximumNumberOfLoadedTiles are needed * to meet the desired screen-space error, determined by {@link Cesium3DTileset#maximumScreenSpaceError}, * for the current view than the number of tiles loaded will exceed @@ -1086,21 +1091,20 @@ define([ } function unloadTiles(tileset, frameState) { + var trimTiles = tileset._trimTiles; + tileset._trimTiles = false; + var stats = tileset._statistics; - var frameNumber = frameState.frameNumber; var maximumNumberOfLoadedTiles = tileset.maximumNumberOfLoadedTiles + 1; // + 1 to account for sentinel var replacementList = tileset._replacementList; -// TODO: explore proactively clearing the cache - automatically or provide a function for the user. -// TODO: could check last n frames using lastTouchedFrameNumber - // 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) && (replacementList.length > maximumNumberOfLoadedTiles)) { + while ((node !== sentinel) && ((replacementList.length > maximumNumberOfLoadedTiles) || trimTiles)) { node.item.unloadContent(); var currentNode = node; @@ -1111,6 +1115,20 @@ define([ } } + /** + * Unloads all tiles that weren't selected the previous frame. This can be used to + * explicitly manage the tile cache and reduce the total number of tiles loaded below + * {@link Cesium3DTileset#maximumNumberOfLoadedTiles}. + *

+ * Tile unloads occur at the next frame to keep all the WebGL delete calls + * within the render loop. + *

+ */ + Cesium3DTileset.prototype.trimLoadedTiles = function() { + // Defer to next frame so WebGL delete calls happen inside the render loop + this._trimTiles = true; + }; + /////////////////////////////////////////////////////////////////////////// function raiseLoadProgressEvent(tileset, frameState) { From 00ebcfbd307750845da91805009a2cdeee77ee86 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 14:41:27 -0400 Subject: [PATCH 11/22] Updated Sandcastle app --- Apps/Sandcastle/gallery/3D Tiles.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Apps/Sandcastle/gallery/3D Tiles.html b/Apps/Sandcastle/gallery/3D Tiles.html index 3c9f61df7e10..342979c620bd 100644 --- a/Apps/Sandcastle/gallery/3D Tiles.html +++ b/Apps/Sandcastle/gallery/3D Tiles.html @@ -52,7 +52,9 @@ reset(); tileset = scene.primitives.add(new Cesium.Cesium3DTileset({ - url : url + url : url, + debugShowStatistics : true, + maximumNumberOfLoadedTiles : 3 })); return Cesium.when(tileset.readyPromise).then(function(tileset) { @@ -103,7 +105,7 @@ return; } - console.log('Loading: requests: ' + numberOfPendingRequests + ', processing: ' + numberProcessing); + //console.log('Loading: requests: ' + numberOfPendingRequests + ', processing: ' + numberProcessing); }); addExpressionUI(); @@ -374,6 +376,10 @@ console.log('New max SSE: ' + tileset.maximumScreenSpaceError); }); +Sandcastle.addToolbarButton('Trim tiles (cache)', function() { + tileset.trimLoadedTiles(); +}); + // Styling //////////////////////////////////////////////////////////////////// var numberofColors = 6; From 6b648caacd6f44c87e809c513a5f829b817dcb6e Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 14:44:59 -0400 Subject: [PATCH 12/22] Remove unused lastTouchedFrameNumber --- Source/Scene/Cesium3DTile.js | 12 ------------ Source/Scene/Cesium3DTileset.js | 5 ++--- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 40ba4b1c7134..8f1096469c02 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -279,17 +279,6 @@ define([ */ this.lastSelectedFrameNumber = 0; - /** - * The last frame number the tile was visited during tile selection. A tile may be touched, - * but not selected because, for example, it is a parent using replacement refinement and - * its children are selected. A selected tile will always be touched. - * - * @type {Number} - * - * @private - */ - this.lastTouchedFrameNumber = 0; - /** * The time when a style was last applied to this tile. * @@ -441,7 +430,6 @@ define([ this.parentPlaneMask = 0; this.selected = false; this.lastSelectedFrameNumber = 0; - this.lastTouchedFrameNumber = 0; this.lastStyleTime = 0; this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 9d6b1123ed97..b28898d9593c 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -727,10 +727,9 @@ define([ } } - function touch(tileset, tile, frameState) { + function touch(tileset, tile) { var node = tile.replacementNode; if (defined(node)) { - tile.lastTouchedFrameNumber = frameState.frameNumber; tileset._replacementList.splice(tileset._replacementSentinel, node); } } @@ -789,7 +788,7 @@ define([ } var fullyVisible = (planeMask === CullingVolume.MASK_INSIDE); - touch(tileset, t, frameState); + touch(tileset, t); // Tile is inside/intersects the view frustum. How many pixels is its geometric error? var sse = getScreenSpaceError(t.geometricError, t, frameState); From 2520cdcd90d9d7f38abb5af88375137d03050b04 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 5 Apr 2016 14:50:43 -0400 Subject: [PATCH 13/22] Fix JSHint warnings --- Source/Core/DoublyLinkedList.js | 2 +- Source/Scene/Cesium3DTileset.js | 2 +- Specs/Scene/Cesium3DTilesetSpec.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/Core/DoublyLinkedList.js b/Source/Core/DoublyLinkedList.js index 02dd8d86ffbd..02afaae6ec54 100644 --- a/Source/Core/DoublyLinkedList.js +++ b/Source/Core/DoublyLinkedList.js @@ -105,7 +105,7 @@ define([ nextNode.next = oldNodeNext; nextNode.previous = node; - } + }; return DoublyLinkedList; }); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index b28898d9593c..90567ba92010 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -344,7 +344,7 @@ define([ this.numberTotal = -1; this.numberOfTilesStyled = -1; this.numberOfFeaturesStyled = -1; - }; + } defineProperties(Cesium3DTileset.prototype, { /** diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 7e4d15a1452f..c41ce3574f9f 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -874,7 +874,6 @@ defineSuite([ viewNothing(); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { var root = tileset._root; - var content = root.content; viewRootOnly(); scene.renderForSpecs(); // Request root From 78579d964ad7ee12e2c20873e12ab327fad1a5e6 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 07:51:39 -0400 Subject: [PATCH 14/22] Add reference doc --- Source/Scene/Cesium3DTile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 8f1096469c02..759a0f5cc482 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -235,7 +235,10 @@ define([ this.hasTilesetContent = hasTilesetContent; /** - * DOC_TBA + * The corresponding node in the cache replacement list. + * + * @type {DoublyLinkedList} + * @readonly * * @private */ From 3d24ecfa65775c3b1b05daf8a47a94f7200a9688 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 07:51:50 -0400 Subject: [PATCH 15/22] Add tileUnload event --- Source/Scene/Cesium3DTileset.js | 47 ++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 90567ba92010..e5bb226c1a3a 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -51,15 +51,24 @@ define([ SceneMode) { 'use strict'; -// TODO: unload events // TODO: unit tests // TODO: test with replacement refinement + +// TODO: since the show/color/setProperty values set with Cesium3DTileFeature only have the +// lifetime of the tile's content (e.g., if the content is unloaded, then reloaded later, the +// values wll be gown), we need to expose an event like loadProgress for when content is unloaded. +// +// We might also want to keep a separate data structure - or flag - so we know what property +// values changed (other than those derived from declarative styling, which can easily be +// reapplied). + // TODO: Refactor TileReplacementQueue to use DoublyLinkedList? // TODO: track stats for cache trashing // TODO: research strategies for proactive cache trimming: number of seconds/frame a tile was not selected, how far out of view a tile is, etc. // TODO: good default for maximumNumberOfLoadedTiles // TODO: More precise size than number of tiles? Count composite tiles as more? Include geometry/texture cost with each tile? // TODO: unload sub-trees from tiles with tileset.json content +// TODO: vertex/texture cache across tiles /** * A {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles tileset}, @@ -286,13 +295,29 @@ define([ */ this.loadProgress = new Event(); - // TODO: since the show/color/setProperty values set with Cesium3DTileFeature only have the - // lifetime of the tile's content (e.g., if the content is unloaded, then reloaded later, the - // values wll be gown), we need to expose an event like loadProgress for when content is unloaded. - // - // We might also want to keep a separate data structure - or flag - so we know what property - // values changed (other than those derived from declarative styling, which can easily be - // reapplied). + /** + * The event fired to indicate that a tile's content was unloaded from the cache. + *

+ * The unloaded {@link Cesium3DTile} is passed to the event listener. + *

+ *

+ * This event is immediately before the tile's content is unloaded while the frame is being + * rendered so that the event listener has access to the tile's content. Do not create + * or modify Cesium entities or primitives during the event listener. + *

+ * + * @type {Event} + * @default new Event() + * + * @example + * tileset.tileUnload.addEventListener(function(tile) { + * console.log('A tile was unloaded from the cache.'); + * }); + * + * @see Cesium3DTileset#maximumNumberOfLoadedTiles + * @see Cesium3DTileset#trimLoadedTiles + */ + this.tileUnload = new Event(); /** * This event fires once for each visible tile in a frame. This can be used to manually @@ -1096,6 +1121,7 @@ define([ var stats = tileset._statistics; var maximumNumberOfLoadedTiles = tileset.maximumNumberOfLoadedTiles + 1; // + 1 to account for sentinel var replacementList = tileset._replacementList; + var tileUnload = tileset.tileUnload; // Traverse the list only to the sentinel since tiles/nodes to the // right of the sentinel were used this frame. @@ -1104,7 +1130,10 @@ define([ var sentinel = tileset._replacementSentinel; var node = replacementList.head; while ((node !== sentinel) && ((replacementList.length > maximumNumberOfLoadedTiles) || trimTiles)) { - node.item.unloadContent(); + var tile = node.item; + + tileUnload.raiseEvent(tile); + tile.unloadContent(); var currentNode = node; node = node.next; From 4f352cf8d2ae9411320a4cea65e4fb95861563d8 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 07:52:01 -0400 Subject: [PATCH 16/22] Update Sandcastle example --- Apps/Sandcastle/gallery/3D Tiles.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Apps/Sandcastle/gallery/3D Tiles.html b/Apps/Sandcastle/gallery/3D Tiles.html index 342979c620bd..001a14b4b0d0 100644 --- a/Apps/Sandcastle/gallery/3D Tiles.html +++ b/Apps/Sandcastle/gallery/3D Tiles.html @@ -108,6 +108,10 @@ //console.log('Loading: requests: ' + numberOfPendingRequests + ', processing: ' + numberProcessing); }); + tileset.tileUnload.addEventListener(function(tile) { + //console.log('Tile unloaded.') + }); + addExpressionUI(); }); } From befe17110668b88d62062472b748f8487a742df7 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 09:48:02 -0400 Subject: [PATCH 17/22] Added cache replacement tests --- Source/Scene/Cesium3DTileset.js | 107 +++++++---- Specs/Scene/Cesium3DTilesetSpec.js | 284 ++++++++++++++++++++++++++++- 2 files changed, 346 insertions(+), 45 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e5bb226c1a3a..78f7464b4cba 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -51,9 +51,6 @@ define([ SceneMode) { 'use strict'; -// TODO: unit tests -// TODO: test with replacement refinement - // TODO: since the show/color/setProperty values set with Cesium3DTileFeature only have the // lifetime of the tile's content (e.g., if the content is unloaded, then reloaded later, the // values wll be gown), we need to expose an event like loadProgress for when content is unloaded. @@ -63,9 +60,9 @@ define([ // reapplied). // TODO: Refactor TileReplacementQueue to use DoublyLinkedList? +// TODO: good default for maximumNumberOfLoadedTiles // TODO: track stats for cache trashing // TODO: research strategies for proactive cache trimming: number of seconds/frame a tile was not selected, how far out of view a tile is, etc. -// TODO: good default for maximumNumberOfLoadedTiles // TODO: More precise size than number of tiles? Count composite tiles as more? Include geometry/texture cost with each tile? // TODO: unload sub-trees from tiles with tileset.json content // TODO: vertex/texture cache across tiles @@ -147,34 +144,8 @@ define([ */ this.show = defaultValue(options.show, true); - /** - * The maximum screen-space error used to drive level-of-detail refinement. Higher - * values will provide better performance but lower visual quality. - * - * @type {Number} - * @default 16 - */ - this.maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); - - /** - * The maximum number of tiles to load. Tiles not in view are unloaded to enforce this. - *

- * If decreasing this value results in unloading tiles, the tiles are unloaded the next frame. - *

- *

- * If more tiles than maximumNumberOfLoadedTiles are needed - * to meet the desired screen-space error, determined by {@link Cesium3DTileset#maximumScreenSpaceError}, - * for the current view than the number of tiles loaded will exceed - * maximumNumberOfLoadedTiles. For example, if the maximum is 128 tiles, but - * 150 tiles are needed to meet the screen-space error, then 150 tiles may be loaded. When - * these tiles go out of view, they will be unloaded. - *

- * - * @type {Number} - * @default 256 - */ - this.maximumNumberOfLoadedTiles = defaultValue(options.maximumNumberOfLoadedTiles, 256); - + this._maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); + this._maximumNumberOfLoadedTiles = defaultValue(options.maximumNumberOfLoadedTiles, 256); this._styleEngine = new Cesium3DTileStyleEngine(); /** @@ -554,6 +525,68 @@ define([ } }, + /** + * The maximum screen-space error used to drive level-of-detail refinement. Higher + * values will provide better performance but lower visual quality. + * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @default 16 + * + * @exception {DeveloperError} maximumScreenSpaceError must be greater than or equal to zero. + */ + maximumScreenSpaceError : { + get : function() { + return this._maximumScreenSpaceError; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + if (value < 0) { + throw new DeveloperError('maximumScreenSpaceError must be greater than or equal to zero'); + } + //>>includeEnd('debug'); + + this._maximumScreenSpaceError = value; + } + }, + + /** + * The maximum number of tiles to load. Tiles not in view are unloaded to enforce this. + *

+ * If decreasing this value results in unloading tiles, the tiles are unloaded the next frame. + *

+ *

+ * If more tiles than maximumNumberOfLoadedTiles are needed + * to meet the desired screen-space error, determined by {@link Cesium3DTileset#maximumScreenSpaceError}, + * for the current view than the number of tiles loaded will exceed + * maximumNumberOfLoadedTiles. For example, if the maximum is 128 tiles, but + * 150 tiles are needed to meet the screen-space error, then 150 tiles may be loaded. When + * these tiles go out of view, they will be unloaded. + *

+ * + * @memberof Cesium3DTileset.prototype + * + * @type {Number} + * @default 256 + * + * @exception {DeveloperError} maximumNumberOfLoadedTiles must be greater than or equal to zero. + */ + maximumNumberOfLoadedTiles : { + get : function() { + return this._maximumNumberOfLoadedTiles; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + if (value < 0) { + throw new DeveloperError('maximumNumberOfLoadedTiles must be greater than or equal to zero'); + } + //>>includeEnd('debug'); + + this._maximumNumberOfLoadedTiles = value; + } + }, + /** * @private */ @@ -767,7 +800,7 @@ define([ return; } - var maximumScreenSpaceError = tileset.maximumScreenSpaceError; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var cullingVolume = frameState.cullingVolume; tileset._selectedTiles.length = 0; @@ -996,7 +1029,11 @@ define([ tileset._processingQueue.splice(index, 1); --tileset._statistics.numberProcessing; ++tileset._statistics.numberReady; - tile.replacementNode = tileset._replacementList.add(tile); + if (tile.hasContent) { + // RESEARCH_IDEA: ability to unload tiles (without content) for an + // external tileset when all the tiles are unloaded. + tile.replacementNode = tileset._replacementList.add(tile); + } } else { // Not in processing queue // For example, when a url request fails and the ready promise is rejected @@ -1119,7 +1156,7 @@ define([ tileset._trimTiles = false; var stats = tileset._statistics; - var maximumNumberOfLoadedTiles = tileset.maximumNumberOfLoadedTiles + 1; // + 1 to account for sentinel + var maximumNumberOfLoadedTiles = tileset._maximumNumberOfLoadedTiles + 1; // + 1 to account for sentinel var replacementList = tileset._replacementList; var tileUnload = tileset.tileUnload; diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index c41ce3574f9f..968f33b466b1 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -423,8 +423,6 @@ defineSuite([ it('additive refinement - selects root when sse is met', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { - tileset._root.refine = Cesium3DTileRefine.ADD; - // Meets screen space error, only root tile is rendered viewRootOnly(); scene.renderForSpecs(); @@ -437,8 +435,6 @@ defineSuite([ it('additive refinement - selects all tiles when sse is not met', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { - tileset._root.refine = Cesium3DTileRefine.ADD; - // Does not meet screen space error, all tiles are visible viewAllTiles(); scene.renderForSpecs(); @@ -883,6 +879,9 @@ defineSuite([ }); }); + /////////////////////////////////////////////////////////////////////////// + // Styling tests + it('applies show style to a tileset', function() { return Cesium3DTilesTester.loadTileset(scene, withoutBatchTableUrl).then(function(tileset) { var showColor = scene.renderForSpecs(); @@ -983,12 +982,6 @@ defineSuite([ expect(color).toEqual(originalColor); } - xit('applies color style to a tileset with translucent tiles', function() { - return Cesium3DTilesTester.loadTileset(scene, translucentUrl).then(function(tileset) { - expectColorStyle(tileset); - }); - }); - it('applies color style to a tileset with translucent tiles', function() { return Cesium3DTilesTester.loadTileset(scene, translucentUrl).then(function(tileset) { expectColorStyle(tileset); @@ -1127,4 +1120,275 @@ defineSuite([ }); }); + /////////////////////////////////////////////////////////////////////////// + // Cache replacement tests + + it('Unload all cached tiles not required to meet SSE', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 1; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. This unloads + // the four children since the max number of loaded tiles is one. + viewRootOnly(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(1); + + // Zoom back in so all four children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + }); + }); + }); + + it('Unload some cached tiles not required to meet SSE', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 3; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. This unloads + // two of the four children so three tiles are still loaded (the + // root and two children) since the max number of loaded tiles is three. + viewRootOnly(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(3); + + // Zoom back in so the two children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + }); + }); + }); + + it('Unloads cached tiles outside of the view frustum', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 0; + + scene.renderForSpecs(); + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); + + // Orient camera to face the sky + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude, 100); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 10.0)); + + // All tiles are unloaded + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(0); + expect(stats.numberReady).toEqual(0); + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); + }); + }); + }); + + it('Unloads cached tiles in a tileset with external tileset.json', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { + var stats = tileset._statistics; + var replacementList = tileset._replacementList; + + tileset.maximumNumberOfLoadedTiles = 2; + + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(7); // 5 with b3dm content + 2 empty/tileset.json + expect(replacementList.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 + viewRootOnly(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(4); // 2 with b3dm content + 2 empty/tileset.json + expect(replacementList.length - 1).toEqual(2); + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(7); + expect(replacementList.length - 1).toEqual(5); + }); + }); + }); + + it('Unloads cached tiles in a tileset with empty tiles', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetEmptyRootUrl).then(function(tileset) { + var stats = tileset._statistics; + var replacementList = tileset._replacementList; + + tileset.maximumNumberOfLoadedTiles = 2; + + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(4); + expect(stats.numberReady).toEqual(4); // 4 children with b3dm content (does not include empty root) + + // Orient camera to face the sky + var center = Cartesian3.fromRadians(centerLongitude, centerLatitude, 100); + scene.camera.lookAt(center, new HeadingPitchRange(0.0, 1.57, 10.0)); + + // Unload tiles to meet cache size + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(0); + expect(stats.numberReady).toEqual(2); // 2 children with b3dm content (does not include empty root) + + // Reset camera so all tiles are reloaded + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(4); + expect(stats.numberReady).toEqual(4); + }); + }); + }); + + it('Unload cached tiles when a tileset uses replacement refinement', function() { + // No children have content, but all grandchildren have content + // + // C + // E E + // C C C C + // + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 1; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(4); // 4 grandchildren. Root is replaced. + expect(stats.numberReady).toEqual(5); // Root + four grandchildren (does not include empty children) + + // Zoom out so only root tile is needed to meet SSE. This unloads + // two of the four children so three tiles are still loaded (the + // root and two children) since the max number of loaded tiles is three. + viewRootOnly(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(1); + + // Zoom back in so the two children are re-requested. + viewAllTiles(); + + return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(stats.numberOfCommands).toEqual(4); + expect(stats.numberReady).toEqual(5); + }); + }); + }); + + it('Explicitly unloads cached tiles with trimLoadedTiles', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 5; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. The children + // are not unloaded since max number of loaded tiles is five. + viewRootOnly(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(5); + + tileset.trimLoadedTiles(); + scene.renderForSpecs(); + + expect(stats.numberOfCommands).toEqual(1); + expect(stats.numberReady).toEqual(1); + }); + }); + + it('tileUnload event is raised', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + tileset.maximumNumberOfLoadedTiles = 1; + + // Render parent and four children (using additive refinement) + viewAllTiles(); + scene.renderForSpecs(); + + var stats = tileset._statistics; + expect(stats.numberOfCommands).toEqual(5); + expect(stats.numberReady).toEqual(5); // Five loaded tiles + + // Zoom out so only root tile is needed to meet SSE. All the + // children are unloaded since max number of loaded tiles is one. + viewRootOnly(); + var spyUpdate = jasmine.createSpy('listener'); + tileset.tileUnload.addEventListener(spyUpdate); + scene.renderForSpecs(); + + expect(tileset._root.visibility(scene.frameState.cullingVolume)).not.toEqual(CullingVolume.MASK_OUTSIDE); + expect(spyUpdate.calls.count()).toEqual(4); + expect(spyUpdate.calls.argsFor(0)[0]).toBe(tileset._root.children[0]); + expect(spyUpdate.calls.argsFor(1)[0]).toBe(tileset._root.children[1]); + expect(spyUpdate.calls.argsFor(2)[0]).toBe(tileset._root.children[2]); + expect(spyUpdate.calls.argsFor(3)[0]).toBe(tileset._root.children[3]); + }); + }); + + it('maximumNumberOfLoadedTiles throws when negative', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + tileset.maximumNumberOfLoadedTiles = -1; + }).toThrowDeveloperError(); + }); + + it('maximumScreenSpaceError throws when negative', function() { + var tileset = new Cesium3DTileset({ + url : tilesetUrl + }); + expect(function() { + tileset.maximumScreenSpaceError = -1; + }).toThrowDeveloperError(); + }); + }, 'WebGL'); From 98e50d4770156d00d5d585f71f24c1bda090287f Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 11:36:06 -0400 Subject: [PATCH 18/22] Fix tests when all tests are ran --- Specs/Scene/Cesium3DTilesetSpec.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 968f33b466b1..1453c24a1cc1 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -673,9 +673,6 @@ defineSuite([ spyOn(console, 'log'); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { - scene.renderForSpecs(); - expect(console.log).not.toHaveBeenCalled(); - tileset.debugShowStatistics = true; scene.renderForSpecs(); expect(console.log).toHaveBeenCalled(); @@ -686,9 +683,6 @@ defineSuite([ spyOn(console, 'log'); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { - scene.pickForSpecs(); - expect(console.log).not.toHaveBeenCalled(); - tileset.debugShowPickStatistics = true; scene.pickForSpecs(); expect(console.log).toHaveBeenCalled(); From 71fa03ffccbf20e6b615c5c2c4008073349d2662 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Wed, 6 Apr 2016 11:42:13 -0400 Subject: [PATCH 19/22] Remove TODOs that are now part of the roadmap --- Source/Scene/Cesium3DTileset.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 78f7464b4cba..3bec165c8557 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -51,22 +51,6 @@ define([ SceneMode) { 'use strict'; -// TODO: since the show/color/setProperty values set with Cesium3DTileFeature only have the -// lifetime of the tile's content (e.g., if the content is unloaded, then reloaded later, the -// values wll be gown), we need to expose an event like loadProgress for when content is unloaded. -// -// We might also want to keep a separate data structure - or flag - so we know what property -// values changed (other than those derived from declarative styling, which can easily be -// reapplied). - -// TODO: Refactor TileReplacementQueue to use DoublyLinkedList? -// TODO: good default for maximumNumberOfLoadedTiles -// TODO: track stats for cache trashing -// TODO: research strategies for proactive cache trimming: number of seconds/frame a tile was not selected, how far out of view a tile is, etc. -// TODO: More precise size than number of tiles? Count composite tiles as more? Include geometry/texture cost with each tile? -// TODO: unload sub-trees from tiles with tileset.json content -// TODO: vertex/texture cache across tiles - /** * A {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/README.md|3D Tiles tileset}, * used for streaming massive heterogeneous 3D geospatial datasets. From 9d942a47fe6dd8ed9c586d3da88f66810b8fd32f Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 7 Apr 2016 15:46:08 -0400 Subject: [PATCH 20/22] Small tweaks --- Source/Scene/Cesium3DTileset.js | 4 ++-- Specs/Core/DoublyLinkedListSpec.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 3bec165c8557..a1a3162d0598 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -116,7 +116,7 @@ define([ // [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._replacementList = replacementList; // Tiles with content loaded. For cache management. this._replacementSentinel = replacementList.add(); this._trimTiles = false; @@ -256,7 +256,7 @@ define([ * The unloaded {@link Cesium3DTile} is passed to the event listener. *

*

- * This event is immediately before the tile's content is unloaded while the frame is being + * This event is fired immediately before the tile's content is unloaded while the frame is being * rendered so that the event listener has access to the tile's content. Do not create * or modify Cesium entities or primitives during the event listener. *

diff --git a/Specs/Core/DoublyLinkedListSpec.js b/Specs/Core/DoublyLinkedListSpec.js index e66cd339f3da..9cd6fceaefd7 100644 --- a/Specs/Core/DoublyLinkedListSpec.js +++ b/Specs/Core/DoublyLinkedListSpec.js @@ -2,7 +2,7 @@ defineSuite([ 'Core/DoublyLinkedList' ], function( - DoublyLinkedList) { + DoublyLinkedList) { 'use strict'; it('constructs', function() { @@ -103,7 +103,7 @@ defineSuite([ var list = new DoublyLinkedList(); var node = list.add(1); var node2 = list.add(2); - var node3 = list.add(2); + var node3 = list.add(3); list.remove(node2); From de9b45ae19c0240ae795877c516fc66d2aa83910 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Mon, 11 Apr 2016 13:54:40 -0400 Subject: [PATCH 21/22] Changes based on review --- Source/Scene/Cesium3DTile.js | 2 +- Specs/Scene/Cesium3DTilesetSpec.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 759a0f5cc482..789fe51d36dc 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -237,7 +237,7 @@ define([ /** * The corresponding node in the cache replacement list. * - * @type {DoublyLinkedList} + * @type {DoublyLinkedListNode} * @readonly * * @private diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 1453c24a1cc1..cdb74467b7f4 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1292,15 +1292,14 @@ defineSuite([ expect(stats.numberReady).toEqual(5); // Root + four grandchildren (does not include empty children) // Zoom out so only root tile is needed to meet SSE. This unloads - // two of the four children so three tiles are still loaded (the - // root and two children) since the max number of loaded tiles is three. + // all grandchildren since the max number of loaded tiles is one. viewRootOnly(); scene.renderForSpecs(); expect(stats.numberOfCommands).toEqual(1); expect(stats.numberReady).toEqual(1); - // Zoom back in so the two children are re-requested. + // Zoom back in so the four children are re-requested. viewAllTiles(); return Cesium3DTilesTester.waitForPendingRequests(scene, tileset).then(function() { From 515f3bd05aebe679872943c3ed0956773f5b2ed4 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Tue, 12 Apr 2016 10:56:19 -0400 Subject: [PATCH 22/22] Updates based on review --- Specs/Scene/Cesium3DTilesetSpec.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index cdb74467b7f4..a9bd3173e388 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -846,6 +846,7 @@ defineSuite([ viewNothing(); return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { var root = tileset._root; + var content = root.content; viewRootOnly(); scene.renderForSpecs(); // Request external tileset.json @@ -854,9 +855,13 @@ defineSuite([ expect(stats.numberOfPendingRequests).toEqual(1); scene.primitives.remove(tileset); - expect(root.content).not.toBeDefined(); - // Expect the root to not have added any children from the external tileset.json - expect(root.children.length).toEqual(0); + return content.readyPromise.then(function(root) { + fail('should not resolve'); + }).otherwise(function(error) { + // Expect the root to not have added any children from the external tileset.json + expect(root.children.length).toEqual(0); + expect(RequestScheduler.getNumberOfAvailableRequests()).toEqual(RequestScheduler.maximumRequests); + }); }); }); @@ -864,12 +869,18 @@ defineSuite([ viewNothing(); return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { var root = tileset._root; + var content = root.content; viewRootOnly(); scene.renderForSpecs(); // Request root scene.primitives.remove(tileset); - expect(root.content).not.toBeDefined(); + return content.readyPromise.then(function(root) { + fail('should not resolve'); + }).otherwise(function(error) { + expect(content.state).toEqual(Cesium3DTileContentState.FAILED); + expect(RequestScheduler.getNumberOfAvailableRequests()).toEqual(RequestScheduler.maximumRequests); + }); }); });