diff --git a/Apps/Sandcastle/gallery/development/Terrain Performance.html b/Apps/Sandcastle/gallery/development/Terrain Performance.html
new file mode 100644
index 00000000000..672417f14b9
--- /dev/null
+++ b/Apps/Sandcastle/gallery/development/Terrain Performance.html	
@@ -0,0 +1,189 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
+    <meta name="description" content="Measure terrain loading and rendering performance.">
+    <meta name="cesium-sandcastle-labels" content="Development">
+    <title>Cesium Demo</title>
+    <script type="text/javascript" src="../Sandcastle-header.js"></script>
+    <script type="text/javascript" src="../../../ThirdParty/requirejs-2.1.20/require.js"></script>
+    <script type="text/javascript">
+        if(typeof require === "function") {
+            require.config({
+                baseUrl : '../../../Source',
+                waitSeconds : 120
+            });
+        }
+    </script>
+</head>
+<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
+<style>
+    @import url(../templates/bucket.css);
+</style>
+<div id="cesiumContainer" class="fullSize"></div>
+<div id="loadingOverlay"><h1>Loading...</h1></div>
+<div id="toolbar"></div>
+<script id="cesium_sandcastle_script">
+function startup(Cesium) {
+    'use strict';
+//Sandcastle_Begin
+var viewer = new Cesium.Viewer('cesiumContainer');
+var scene = viewer.scene;
+var camera = scene.camera;
+var globe = scene.globe;
+var statistics = Cesium.RequestScheduler.statistics;
+
+viewer.terrainProvider = Cesium.createWorldTerrain();
+
+var startTime;
+var flightComplete;
+var monitor;
+var minFrameRate = 1000;
+var maxFrameRate = 0;
+var sumFrameRate = 0.0;
+var frameRateSamples = 0;
+
+function startTest() {
+    flightComplete = false;
+    statistics.numberOfActiveRequestsEver = 0;
+    monitor = new Cesium.FrameRateMonitor({
+        scene: scene,
+        samplingWindow: 1.0,
+        quietPeriod: 0.0,
+        warmupPeriod: 0.0,
+        minimumFrameRateDuringWarmup: 0,
+        minimumFrameRateAfterWarmup: 0
+    });
+    scene.preUpdate.addEventListener(measureFrameRate);
+    startTime = window.performance.now();
+    window.setTimeout(function() {
+        scene.postRender.addEventListener(viewReady);
+    }, 500);
+}
+
+function measureFrameRate() {
+    var frameRate = monitor.lastFramesPerSecond;
+    if (frameRate === undefined || frameRate !== frameRate) {
+        return;
+    }
+
+    ++frameRateSamples;
+    sumFrameRate += frameRate;
+    minFrameRate = Math.min(minFrameRate, frameRate);
+    maxFrameRate = Math.max(maxFrameRate, frameRate);
+}
+
+function viewReady(scene, time) {
+    var ready = globe._surface.tileProvider.ready && globe._surface._tileLoadQueueHigh.length === 0 && globe._surface._tileLoadQueueMedium.length === 0 && globe._surface._tileLoadQueueLow.length === 0 && globe._surface._debug.tilesWaitingForChildren === 0;
+    if (flightComplete && ready) {
+        var endTime = window.performance.now();
+        var duration = endTime - startTime;
+        alert((duration / 1000).toFixed(2) + ' seconds ' + statistics.numberOfActiveRequestsEver + ' requests, min/max/avg frame FPS ' + minFrameRate + '/' + maxFrameRate + '/' + (sumFrameRate / frameRateSamples));
+        scene.postRender.removeEventListener(viewReady);
+    }
+}
+
+function goToEverestHorizontal() {
+    camera.position = new Cesium.Cartesian3(302950.1757410969, 5637093.359233209, 2976894.491577989);
+    camera.direction = new Cesium.Cartesian3(-0.9648960658153797, -0.24110066659365145, -0.10414437451009724);
+    camera.right = new Cesium.Cartesian3(-0.02152846103178338, 0.46781654381873394, -0.8835633574877908);
+    camera.up = new Cesium.Cartesian3(-0.26174817580950865, 0.8503047394302772, 0.456584868959543);
+    flightComplete = true;
+}
+
+function goToEverestTopDown() {
+    camera.position = new Cesium.Cartesian3(301989.1870802739, 5637745.915399717, 2977153.0443453398);
+    camera.direction = new Cesium.Cartesian3(0.021398841015326783, -0.8909524564021135, -0.45359211857597476);
+    camera.right = new Cesium.Cartesian3(0.21237352569072232, 0.4473925820246778, -0.8687562161705573);
+    camera.up = new Cesium.Cartesian3(-0.9769542339275126, 0.07774058129659328, -0.19878839712310903);
+    flightComplete = true;
+}
+
+function goToEverest45Degrees() {
+    camera.position = new Cesium.Cartesian3(302760.41072832496, 5637092.977453635, 2977284.6758398763);
+    camera.direction = new Cesium.Cartesian3(-0.7254568510163212, -0.3330925403210976, -0.6022970337764594);
+    camera.right = new Cesium.Cartesian3(0.4750641658993092, 0.39087207931336604, -0.7883736778277414);
+    camera.up = new Cesium.Cartesian3(-0.49802248502640617, 0.8580608237157107, 0.12532049797395203);
+    flightComplete = true;
+}
+
+function zoomToEverest() {
+    var position = new Cesium.Cartesian3(302955.90876054496, 5639614.4908250235, 2981096.1048591887);
+    camera.flyTo({
+        destination : position,
+        easingFunction : Cesium.EasingFunction.QUADRATIC_OUT,
+        complete : function() {
+            flightComplete = true;
+        }
+    });
+}
+
+function panAroundEverest() {
+    camera.position = new Cesium.Cartesian3(302950.1757410969, 5637093.359233209, 2976894.491577989);
+    camera.direction = new Cesium.Cartesian3(-0.9648960658153797, -0.24110066659365145, -0.10414437451009724);
+    camera.right = new Cesium.Cartesian3(-0.02152846103178338, 0.46781654381873394, -0.8835633574877908);
+    camera.up = new Cesium.Cartesian3(-0.26174817580950865, 0.8503047394302772, 0.456584868959543);
+
+    var startCartographic = Cesium.Cartographic.fromCartesian(camera.position);
+    var longitude = startCartographic.longitude;
+    var endLongitude = longitude + 0.01;
+    var latitude = startCartographic.latitude;
+    var height = startCartographic.height;
+    var startTime = window.performance.now();
+    var removeCallback = scene.preRender.addEventListener(function(scene, time) {
+        var endTime = window.performance.now();
+        var delta = endTime - startTime;
+        startTime = endTime;
+        longitude += delta * 0.000001;
+        if (longitude >= endLongitude) {
+            flightComplete = true;
+            removeCallback();
+        }
+        camera.position = Cesium.Cartesian3.fromRadians(longitude, latitude, height);
+    });
+}
+
+Sandcastle.addToolbarButton('Timer Static Horizontal', function() {
+    startTest();
+    goToEverestHorizontal();
+});
+
+Sandcastle.addToolbarButton('Timer Static Top Down', function() {
+    startTest();
+    goToEverestTopDown();
+});
+
+Sandcastle.addToolbarButton('Timer Static 45 degrees', function() {
+    startTest();
+    goToEverest45Degrees();
+});
+
+Sandcastle.addToolbarButton('Timer Zoom', function() {
+    startTest();
+    zoomToEverest();
+});
+
+Sandcastle.addToolbarButton('Timer Pan', function() {
+    startTest();
+    panAroundEverest();
+});
+
+Sandcastle.addToolbarButton('Save camera', function() {
+    var cameraString = 'camera.position = new Cesium.Cartesian3(' + camera.positionWC.x + ', ' + camera.positionWC.y + ', ' + camera.positionWC.z + ');\n'+
+                       'camera.direction = new Cesium.Cartesian3(' + camera.directionWC.x + ', ' + camera.directionWC.y + ', ' + camera.directionWC.z + ');\n'+
+                       'camera.right = new Cesium.Cartesian3(' + camera.rightWC.x + ', ' + camera.rightWC.y + ', ' + camera.rightWC.z + ');\n'+
+                       'camera.up = new Cesium.Cartesian3(' + camera.upWC.x + ', ' + camera.upWC.y + ', ' + camera.upWC.z + ');\n';
+    console.log(cameraString);
+});//Sandcastle_End
+    Sandcastle.finishedLoading();
+}
+if (typeof Cesium !== "undefined") {
+    startup(Cesium);
+} else if (typeof require === "function") {
+    require(["Cesium"], startup);
+}
+</script>
+</body>
+</html>
diff --git a/Apps/Sandcastle/gallery/development/Terrain Tweaks.html b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html
new file mode 100644
index 00000000000..31248dce376
--- /dev/null
+++ b/Apps/Sandcastle/gallery/development/Terrain Tweaks.html	
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
+    <meta name="description" content="Tweak various terrain tweakables.">
+    <meta name="cesium-sandcastle-labels" content="Development">
+    <title>Cesium Demo</title>
+    <script type="text/javascript" src="../Sandcastle-header.js"></script>
+    <script type="text/javascript" src="../../../ThirdParty/requirejs-2.1.20/require.js"></script>
+    <script type="text/javascript">
+        if(typeof require === 'function') {
+            require.config({
+                baseUrl : '../../../Source',
+                waitSeconds : 120
+            });
+        }
+    </script>
+</head>
+<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
+<style>
+    @import url(../templates/bucket.css);
+    #toolbar {
+        background: rgba(42, 42, 42, 0.8);
+        padding: 4px;
+        border-radius: 4px;
+    }
+    #toolbar input {
+        vertical-align: middle;
+        padding-top: 2px;
+        padding-bottom: 2px;
+    }
+</style>
+<div id="cesiumContainer" class="fullSize"></div>
+<div id="loadingOverlay"><h1>Loading...</h1></div>
+<div id="toolbar">
+    <table>
+        <tbody>
+            <tr>
+                <td>Loading Descendant Limit</td>
+                <td>
+                    <input type="text" size="5" data-bind="value: loadingDescendantLimit" title="The number of loading descendant tiles that is considered 'too many'. If a tile has too many loading descendants, that tile will be loaded and rendered before any of its descendants are loaded and rendered. This means more feedback for the user that something is happening at the cost of a longer overall load time. Setting this to 0 will cause each tile level to be loaded successively, significantly increasing load time. Setting it to a large number (e.g. 100000) will minimize the number of tiles that are loaded but tend to make detail appear all at once after a long wait." />
+                </td>
+            </tr>
+            <tr>
+                <td>Preload Ancestors</td>
+                <td>
+                    <input type="checkbox" data-bind="checked: preloadAncestors" title="Indicating whether the ancestors of rendered tiles should be preloaded. Setting this to true optimizes the zoom-out experience and provides more detail in newly-exposed areas when panning. The down side is that it requires loading more tiles." />
+                </td>
+            </tr>
+            <tr>
+                <td>Preload Siblings</td>
+                <td>
+                    <input type="checkbox" data-bind="checked: preloadSiblings" title="Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded. Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even if they are culled. Setting this to true may provide a better panning experience at the cost of loading more tiles." />
+                </td>
+            </tr>
+            <tr>
+                <td>Fill Tile Highlight</td>
+                <td>
+                    <input type="checkbox" data-bind="checked: fillHighlightEnabled" title="Highlights generated fill tiles." />
+                    <input type="text" size="25" data-bind="value: fillHighlightColor" title="The color to use to highlight fill tiles. If undefined, fill tiles are not highlighted at all. The alpha value is used to alpha blend with the tile's actual color." />
+                </td>
+            </tr>
+        </tbody>
+    </table>
+</div>
+<script id="cesium_sandcastle_script">
+function startup(Cesium) {
+    'use strict';
+//Sandcastle_Begin
+var viewer = new Cesium.Viewer('cesiumContainer');
+
+var viewModel = {
+    loadingDescendantLimit: viewer.scene.globe.loadingDescendantLimit,
+    preloadAncestors: viewer.scene.globe.preloadAncestors,
+    preloadSiblings: viewer.scene.globe.preloadSiblings,
+    fillHighlightColor: Cesium.defined(viewer.scene.globe.fillHighlightColor) ? viewer.scene.globe.fillHighlightColor.toCssColorString() : 'rgba(255, 255, 0, 0.5)',
+    fillHighlightEnabled: Cesium.defined(viewer.scene.globe.fillHighlightColor)
+};
+
+Cesium.knockout.track(viewModel);
+
+var toolbar = document.getElementById('toolbar');
+Cesium.knockout.applyBindings(viewModel, toolbar);
+
+Cesium.knockout.getObservable(viewModel, 'loadingDescendantLimit').subscribe(function(newValue) {
+    viewer.scene.globe.loadingDescendantLimit = parseInt(newValue, 10);
+});
+Cesium.knockout.getObservable(viewModel, 'preloadAncestors').subscribe(function(newValue) {
+    viewer.scene.globe.preloadAncestors = newValue;
+});
+Cesium.knockout.getObservable(viewModel, 'preloadSiblings').subscribe(function(newValue) {
+    viewer.scene.globe.preloadSiblings = newValue;
+});
+
+function updateFillHighlight() {
+    if (viewModel.fillHighlightEnabled) {
+        viewer.scene.globe.fillHighlightColor = Cesium.Color.fromCssColorString(viewModel.fillHighlightColor);
+    } else {
+        viewer.scene.globe.fillHighlightColor = undefined;
+    }
+}
+
+Cesium.knockout.getObservable(viewModel, 'fillHighlightEnabled').subscribe(function(newValue) {
+    updateFillHighlight();
+});
+Cesium.knockout.getObservable(viewModel, 'fillHighlightColor').subscribe(function(newValue) {
+    updateFillHighlight();
+});
+
+//Sandcastle_End
+    Sandcastle.finishedLoading();
+}
+if (typeof Cesium !== 'undefined') {
+    startup(Cesium);
+} else if (typeof require === 'function') {
+    require(['Cesium'], startup);
+}
+</script>
+</body>
+</html>
diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js
index 447f7b2ce69..1e003686d19 100644
--- a/Source/Core/CesiumTerrainProvider.js
+++ b/Source/Core/CesiumTerrainProvider.js
@@ -904,7 +904,10 @@ define([
          * Gets an object that can be used to determine availability of terrain from this provider, such as
          * at points and in rectangles.  This function should not be called before
          * {@link CesiumTerrainProvider#ready} returns true.  This property may be undefined if availability
-         * information is not available.
+         * information is not available. Note that this reflects tiles that are known to be available currently.
+         * Additional tiles may be discovered to be available in the future, e.g. if availability information
+         * exists deeper in the tree rather than it all being discoverable at the root. However, a tile that
+         * is available now will not become unavailable in the future.
          * @memberof CesiumTerrainProvider.prototype
          * @type {TileAvailability}
          */
@@ -936,7 +939,7 @@ define([
      * @param {Number} x The X coordinate of the tile for which to request geometry.
      * @param {Number} y The Y coordinate of the tile for which to request geometry.
      * @param {Number} level The level of the tile for which to request geometry.
-     * @returns {Boolean} Undefined if not supported, otherwise true or false.
+     * @returns {Boolean} Undefined if not supported or availability is unknown, otherwise true or false.
      */
     CesiumTerrainProvider.prototype.getTileDataAvailable = function(x, y, level) {
         if (!defined(this._availability)) {
diff --git a/Source/Core/GoogleEarthEnterpriseTerrainData.js b/Source/Core/GoogleEarthEnterpriseTerrainData.js
index 8fd4ce2ea11..08ce5604a26 100644
--- a/Source/Core/GoogleEarthEnterpriseTerrainData.js
+++ b/Source/Core/GoogleEarthEnterpriseTerrainData.js
@@ -204,7 +204,11 @@ define([
                     result.numberOfAttributes,
                     result.orientedBoundingBox,
                     TerrainEncoding.clone(result.encoding),
-                    exaggeration);
+                    exaggeration,
+                    result.westIndicesSouthToNorth,
+                    result.southIndicesEastToWest,
+                    result.eastIndicesNorthToSouth,
+                    result.northIndicesWestToEast);
 
                 that._vertexCountWithoutSkirts = result.vertexCountWithoutSkirts;
                 that._skirtIndex = result.skirtIndex;
diff --git a/Source/Core/HeightmapTerrainData.js b/Source/Core/HeightmapTerrainData.js
index 905ece4de51..e7f012c04b1 100644
--- a/Source/Core/HeightmapTerrainData.js
+++ b/Source/Core/HeightmapTerrainData.js
@@ -30,7 +30,7 @@ define([
 
     /**
      * Terrain data for a single tile where the terrain data is represented as a heightmap.  A heightmap
-     * is a rectangular array of heights in row-major order from south to north and west to east.
+     * is a rectangular array of heights in row-major order from north to south and west to east.
      *
      * @alias HeightmapTerrainData
      * @constructor
@@ -50,6 +50,9 @@ define([
      *                  <tr><td>2</td><td>4</td><td>Northwest</td></tr>
      *                  <tr><td>3</td><td>8</td><td>Northeast</td></tr>
      *                 </table>
+     * @param {Uint8Array} [options.waterMask] The water mask included in this terrain data, if any.  A water mask is a square
+     *                     Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
+     *                     Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
      * @param {Object} [options.structure] An object describing the structure of the height data.
      * @param {Number} [options.structure.heightScale=1.0] The factor by which to multiply height samples in order to obtain
      *                 the height above the heightOffset, in meters.  The heightOffset is added to the resulting
@@ -152,7 +155,7 @@ define([
             }
         },
         /**
-         * The water mask included in this terrain data, if any.  A water mask is a rectangular
+         * The water mask included in this terrain data, if any.  A water mask is a square
          * Uint8Array or image where a value of 255 indicates water and a value of 0 indicates land.
          * Values in between 0 and 255 are allowed as well to smoothly blend between land and water.
          * @memberof HeightmapTerrainData.prototype
@@ -162,6 +165,12 @@ define([
             get : function() {
                 return this._waterMask;
             }
+        },
+
+        childTileMask : {
+            get : function() {
+                return this._childTileMask;
+            }
         }
     });
 
@@ -244,7 +253,11 @@ define([
                     result.numberOfAttributes,
                     result.orientedBoundingBox,
                     TerrainEncoding.clone(result.encoding),
-                    exaggeration);
+                    exaggeration,
+                    result.westIndicesSouthToNorth,
+                    result.southIndicesEastToWest,
+                    result.eastIndicesNorthToSouth,
+                    result.northIndicesWestToEast);
 
             // Free memory received from server after mesh is created.
             that._buffer = undefined;
@@ -252,6 +265,83 @@ define([
         });
     };
 
+    /**
+     * @private
+     */
+    HeightmapTerrainData.prototype._createMeshSync = function(tilingScheme, x, y, level, exaggeration) {
+        //>>includeStart('debug', pragmas.debug);
+        if (!defined(tilingScheme)) {
+            throw new DeveloperError('tilingScheme is required.');
+        }
+        if (!defined(x)) {
+            throw new DeveloperError('x is required.');
+        }
+        if (!defined(y)) {
+            throw new DeveloperError('y is required.');
+        }
+        if (!defined(level)) {
+            throw new DeveloperError('level is required.');
+        }
+        //>>includeEnd('debug');
+
+        var ellipsoid = tilingScheme.ellipsoid;
+        var nativeRectangle = tilingScheme.tileXYToNativeRectangle(x, y, level);
+        var rectangle = tilingScheme.tileXYToRectangle(x, y, level);
+        exaggeration = defaultValue(exaggeration, 1.0);
+
+        // Compute the center of the tile for RTC rendering.
+        var center = ellipsoid.cartographicToCartesian(Rectangle.center(rectangle));
+
+        var structure = this._structure;
+
+        var levelZeroMaxError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(ellipsoid, this._width, tilingScheme.getNumberOfXTilesAtLevel(0));
+        var thisLevelMaxError = levelZeroMaxError / (1 << level);
+        this._skirtHeight = Math.min(thisLevelMaxError * 4.0, 1000.0);
+
+        var result = HeightmapTessellator.computeVertices({
+            heightmap : this._buffer,
+            structure : structure,
+            includeWebMercatorT : true,
+            width : this._width,
+            height : this._height,
+            nativeRectangle : nativeRectangle,
+            rectangle : rectangle,
+            relativeToCenter : center,
+            ellipsoid : ellipsoid,
+            skirtHeight : this._skirtHeight,
+            isGeographic : tilingScheme.projection instanceof GeographicProjection,
+            exaggeration : exaggeration
+        });
+
+        // Free memory received from server after mesh is created.
+        this._buffer = undefined;
+
+        var arrayWidth = this._width;
+        var arrayHeight = this._height;
+
+        if (this._skirtHeight > 0.0) {
+            arrayWidth += 2;
+            arrayHeight += 2;
+        }
+
+        return new TerrainMesh(
+            center,
+            result.vertices,
+            TerrainProvider.getRegularGridIndices(arrayWidth, arrayHeight),
+            result.minimumHeight,
+            result.maximumHeight,
+            result.boundingSphere3D,
+            result.occludeePointInScaledSpace,
+            result.encoding.getStride(),
+            result.orientedBoundingBox,
+            TerrainEncoding.clone(result.encoding),
+            exaggeration,
+            result.westIndicesSouthToNorth,
+            result.southIndicesEastToWest,
+            result.eastIndicesNorthToSouth,
+            result.northIndicesWestToEast);
+    };
+
     /**
      * Computes the terrain height at a specified longitude and latitude.
      *
@@ -333,6 +423,11 @@ define([
         }
         //>>includeEnd('debug');
 
+        var meshData = this._mesh;
+        if (!defined(meshData)) {
+            return undefined;
+        }
+
         var width = this._width;
         var height = this._height;
         var structure = this._structure;
@@ -340,10 +435,6 @@ define([
         var stride = structure.stride;
 
         var heights = new this._bufferType(width * height * stride);
-        var meshData = this._mesh;
-        if (!defined(meshData)) {
-            return undefined;
-        }
 
         var buffer = meshData.vertices;
         var encoding = meshData.encoding;
diff --git a/Source/Core/HeightmapTessellator.js b/Source/Core/HeightmapTessellator.js
index b7604ce29a8..ab688ede35a 100644
--- a/Source/Core/HeightmapTessellator.js
+++ b/Source/Core/HeightmapTessellator.js
@@ -219,7 +219,7 @@ define([
         var granularityX = rectangleWidth / (width - 1);
         var granularityY = rectangleHeight / (height - 1);
 
-		if (!isGeographic) {
+        if (!isGeographic) {
             rectangleWidth *= oneOverGlobeSemimajorAxis;
             rectangleHeight *= oneOverGlobeSemimajorAxis;
         }
@@ -254,8 +254,8 @@ define([
 
         var hMin = Number.POSITIVE_INFINITY;
 
-        var arrayWidth = width + (skirtHeight > 0.0 ? 2.0 : 0.0);
-        var arrayHeight = height + (skirtHeight > 0.0 ? 2.0 : 0.0);
+        var arrayWidth = width + (skirtHeight > 0.0 ? 2 : 0);
+        var arrayHeight = height + (skirtHeight > 0.0 ? 2 : 0);
         var size = arrayWidth * arrayHeight;
         var positions = new Array(size);
         var heights = new Array(size);
@@ -267,7 +267,7 @@ define([
         var startCol = 0;
         var endCol = width;
 
-        if (skirtHeight > 0) {
+        if (skirtHeight > 0.0) {
             --startRow;
             ++endRow;
             --startCol;
@@ -428,6 +428,41 @@ define([
             bufferIndex = encoding.encode(vertices, bufferIndex, positions[j], uvs[j], heights[j], undefined, webMercatorTs[j]);
         }
 
+        var westIndicesSouthToNorth;
+        var southIndicesEastToWest;
+        var eastIndicesNorthToSouth;
+        var northIndicesWestToEast;
+
+        if (skirtHeight > 0.0) {
+            northIndicesWestToEast = [];
+            southIndicesEastToWest = [];
+            for (var i1 = 0; i1 < width; ++i1) {
+                northIndicesWestToEast.push(arrayWidth + 1 + i1);
+                southIndicesEastToWest.push(arrayWidth * (arrayHeight - 1) - 2 - i1);
+            }
+
+            westIndicesSouthToNorth = [];
+            eastIndicesNorthToSouth = [];
+            for (var i2 = 0; i2 < height; ++i2) {
+                eastIndicesNorthToSouth.push((i2 + 1) * arrayWidth + width);
+                westIndicesSouthToNorth.push((height - i2) * arrayWidth + 1);
+            }
+        } else {
+            northIndicesWestToEast = [];
+            southIndicesEastToWest = [];
+            for (var i3 = 0; i3 < width; ++i3) {
+                northIndicesWestToEast.push(i3);
+                southIndicesEastToWest.push(width * height - 1 - i3);
+            }
+
+            westIndicesSouthToNorth = [];
+            eastIndicesNorthToSouth = [];
+            for (var i4 = 0; i4 < height; ++i4) {
+                eastIndicesNorthToSouth.push((i4 + 1) * width - 1);
+                westIndicesSouthToNorth.push((height - i4 - 1) * width );
+            }
+        }
+
         return {
             vertices : vertices,
             maximumHeight : maximumHeight,
@@ -435,7 +470,11 @@ define([
             encoding : encoding,
             boundingSphere3D : boundingSphere3D,
             orientedBoundingBox : orientedBoundingBox,
-            occludeePointInScaledSpace : occludeePointInScaledSpace
+            occludeePointInScaledSpace : occludeePointInScaledSpace,
+            westIndicesSouthToNorth : westIndicesSouthToNorth,
+            southIndicesEastToWest : southIndicesEastToWest,
+            eastIndicesNorthToSouth : eastIndicesNorthToSouth,
+            northIndicesWestToEast : northIndicesWestToEast
         };
     };
 
diff --git a/Source/Core/IndexDatatype.js b/Source/Core/IndexDatatype.js
index f3ac0e8f0ae..a3bfb2779da 100644
--- a/Source/Core/IndexDatatype.js
+++ b/Source/Core/IndexDatatype.js
@@ -72,6 +72,27 @@ define([
         //>>includeEnd('debug');
     };
 
+    /**
+     * Gets the datatype with a given size in bytes.
+     *
+     * @param {Number} sizeInBytes The size of a single index in bytes.
+     * @returns {IndexDatatype} The index datatype with the given size.
+     */
+    IndexDatatype.fromSizeInBytes = function(sizeInBytes) {
+        switch (sizeInBytes) {
+            case 2:
+                return IndexDatatype.UNSIGNED_SHORT;
+            case 4:
+                return IndexDatatype.UNSIGNED_INT;
+            case 1:
+                return IndexDatatype.UNSIGNED_BYTE;
+            //>>includeStart('debug', pragmas.debug);
+            default:
+                throw new DeveloperError('Size in bytes cannot be mapped to an IndexDatatype');
+            //>>includeEnd('debug');
+        }
+    };
+
     /**
      * Validates that the provided index datatype is a valid {@link IndexDatatype}.
      *
diff --git a/Source/Core/Math.js b/Source/Core/Math.js
index 04494d6484e..c3c2174221d 100644
--- a/Source/Core/Math.js
+++ b/Source/Core/Math.js
@@ -583,6 +583,108 @@ define([
         return absDiff <= absoluteEpsilon || absDiff <= relativeEpsilon * Math.max(Math.abs(left), Math.abs(right));
     };
 
+    /**
+     * Determines if the left value is less than the right value. If the two values are within
+     * <code>absoluteEpsilon</code> of each other, they are considered equal and this function returns false.
+     *
+     * @param {Number} left The first number to compare.
+     * @param {Number} second The second number to compare.
+     * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison.
+     * @returns {Boolean} <code>true</code> if <code>left</code> is less than <code>right</code> by more than
+     *          <code>absoluteEpsilon<code>. <code>false</code> if <code>left</code> is greater or if the two
+     *          values are nearly equal.
+     */
+    CesiumMath.leftIsLessThanRight = function(left, right, absoluteEpsilon) {
+        //>>includeStart('debug', pragmas.debug);
+        if (!defined(left)) {
+            throw new DeveloperError('first is required.');
+        }
+        if (!defined(right)) {
+            throw new DeveloperError('second is required.');
+        }
+        if (!defined(absoluteEpsilon)) {
+            throw new DeveloperError('relativeEpsilon is required.');
+        }
+        //>>includeEnd('debug');
+        return left - right < -absoluteEpsilon;
+    };
+
+    /**
+     * Determines if the left value is less than or equal to the right value. If the two values are within
+     * <code>absoluteEpsilon</code> of each other, they are considered equal and this function returns true.
+     *
+     * @param {Number} left The first number to compare.
+     * @param {Number} second The second number to compare.
+     * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison.
+     * @returns {Boolean} <code>true</code> if <code>left</code> is less than <code>right</code> or if the
+     *          the values are nearly equal.
+     */
+    CesiumMath.leftIsLessThanOrEqualToRight = function(left, right, absoluteEpsilon) {
+        //>>includeStart('debug', pragmas.debug);
+        if (!defined(left)) {
+            throw new DeveloperError('first is required.');
+        }
+        if (!defined(right)) {
+            throw new DeveloperError('second is required.');
+        }
+        if (!defined(absoluteEpsilon)) {
+            throw new DeveloperError('relativeEpsilon is required.');
+        }
+        //>>includeEnd('debug');
+        return left - right < absoluteEpsilon;
+    };
+
+    /**
+     * Determines if the left value is greater the right value. If the two values are within
+     * <code>absoluteEpsilon</code> of each other, they are considered equal and this function returns false.
+     *
+     * @param {Number} left The first number to compare.
+     * @param {Number} second The second number to compare.
+     * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison.
+     * @returns {Boolean} <code>true</code> if <code>left</code> is greater than <code>right</code> by more than
+     *          <code>absoluteEpsilon<code>. <code>false</code> if <code>left</code> is less or if the two
+     *          values are nearly equal.
+     */
+    CesiumMath.leftIsGreaterThanRight = function(left, right, absoluteEpsilon) {
+        //>>includeStart('debug', pragmas.debug);
+        if (!defined(left)) {
+            throw new DeveloperError('first is required.');
+        }
+        if (!defined(right)) {
+            throw new DeveloperError('second is required.');
+        }
+        if (!defined(absoluteEpsilon)) {
+            throw new DeveloperError('relativeEpsilon is required.');
+        }
+        //>>includeEnd('debug');
+        return left - right > absoluteEpsilon;
+    };
+
+    /**
+     * Determines if the left value is greater than or equal to the right value. If the two values are within
+     * <code>absoluteEpsilon</code> of each other, they are considered equal and this function returns true.
+     *
+     * @param {Number} left The first number to compare.
+     * @param {Number} second The second number to compare.
+     * @param {Number} absoluteEpsilon The absolute epsilon to use in comparison.
+     * @returns {Boolean} <code>true</code> if <code>left</code> is greater than <code>right</code> or if the
+     *          the values are nearly equal.
+     */
+    CesiumMath.leftIsGreaterThanOrEqualToRight = function(left, right, absoluteEpsilon) {
+        //>>includeStart('debug', pragmas.debug);
+        if (!defined(left)) {
+            throw new DeveloperError('first is required.');
+        }
+        if (!defined(right)) {
+            throw new DeveloperError('second is required.');
+        }
+        if (!defined(absoluteEpsilon)) {
+            throw new DeveloperError('relativeEpsilon is required.');
+        }
+        //>>includeEnd('debug');
+        return left - right > -absoluteEpsilon;
+    };
+
     var factorials = [1];
 
     /**
diff --git a/Source/Core/QuantizedMeshTerrainData.js b/Source/Core/QuantizedMeshTerrainData.js
index 7040125f2ef..35bca0020b5 100644
--- a/Source/Core/QuantizedMeshTerrainData.js
+++ b/Source/Core/QuantizedMeshTerrainData.js
@@ -221,6 +221,18 @@ define([
             get : function() {
                 return this._waterMask;
             }
+        },
+
+        childTileMask : {
+            get : function() {
+                return this._childTileMask;
+            }
+        },
+
+        canUpsample : {
+            get : function() {
+                return defined(this._mesh);
+            }
         }
     });
 
@@ -334,7 +346,11 @@ define([
                     stride,
                     obb,
                     terrainEncoding,
-                    exaggeration);
+                    exaggeration,
+                    result.westIndicesSouthToNorth,
+                    result.southIndicesEastToWest,
+                    result.eastIndicesNorthToSouth,
+                    result.northIndicesWestToEast);
 
             // Free memory received from server after mesh is created.
             that._quantizedVertices = undefined;
diff --git a/Source/Core/Request.js b/Source/Core/Request.js
index 44b51c9954d..256ca55bdac 100644
--- a/Source/Core/Request.js
+++ b/Source/Core/Request.js
@@ -31,7 +31,7 @@ define([
         options = defaultValue(options, defaultValue.EMPTY_OBJECT);
 
         var throttleByServer = defaultValue(options.throttleByServer, false);
-        var throttle = throttleByServer || defaultValue(options.throttle, false);
+        var throttle = defaultValue(options.throttle, false);
 
         /**
          * The URL to request.
diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js
index 6fd455b5800..a60a5696efe 100644
--- a/Source/Core/RequestScheduler.js
+++ b/Source/Core/RequestScheduler.js
@@ -342,6 +342,11 @@ define([
             request.serverKey = RequestScheduler.getServerKey(request.url);
         }
 
+        if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) {
+            // Server is saturated. Try again later.
+            return undefined;
+        }
+
         if (!RequestScheduler.throttleRequests || !request.throttle) {
             return startRequest(request);
         }
@@ -351,11 +356,6 @@ define([
             return undefined;
         }
 
-        if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) {
-            // Server is saturated. Try again later.
-            return undefined;
-        }
-
         // Insert into the priority heap and see if a request was bumped off. If this request is the lowest
         // priority it will be returned.
         updatePriority(request);
diff --git a/Source/Core/TerrainEncoding.js b/Source/Core/TerrainEncoding.js
index 332fdf0ebd8..21bd2273dfb 100644
--- a/Source/Core/TerrainEncoding.js
+++ b/Source/Core/TerrainEncoding.js
@@ -45,7 +45,7 @@ define([
      * @private
      */
     function TerrainEncoding(axisAlignedBoundingBox, minimumHeight, maximumHeight, fromENU, hasVertexNormals, hasWebMercatorT) {
-        var quantization;
+        var quantization = TerrainQuantization.NONE;
         var center;
         var toENU;
         var matrix;
@@ -248,6 +248,16 @@ define([
         return buffer[index + 3];
     };
 
+    TerrainEncoding.prototype.decodeWebMercatorT = function(buffer, index) {
+        index *= this.getStride();
+
+        if (this.quantization === TerrainQuantization.BITS12) {
+            return AttributeCompression.decompressTextureCoordinates(buffer[index + 3], cartesian2Scratch).x;
+        }
+
+        return buffer[index + 6];
+    };
+
     TerrainEncoding.prototype.getOctEncodedNormal = function(buffer, index, result) {
         var stride = this.getStride();
         index = (index + 1) * stride - 1;
diff --git a/Source/Core/TerrainMesh.js b/Source/Core/TerrainMesh.js
index cffa35b8c7c..0b2683c01d3 100644
--- a/Source/Core/TerrainMesh.js
+++ b/Source/Core/TerrainMesh.js
@@ -1,6 +1,8 @@
 define([
+        './Cartesian3',
         './defaultValue'
     ], function(
+        Cartesian3,
         defaultValue) {
     'use strict';
 
@@ -16,7 +18,7 @@ define([
       *                       The vertex data is in the order [X, Y, Z, H, U, V], where X, Y, and Z represent
       *                       the Cartesian position of the vertex, H is the height above the ellipsoid, and
       *                       U and V are the texture coordinates.
-      * @param {Uint16Array|Uint32Array} indices The indices describing how the vertices are connected to form triangles.
+      * @param {Uint8Array|Uint16Array|Uint32Array} indices The indices describing how the vertices are connected to form triangles.
       * @param {Number} minimumHeight The lowest height in the tile, in meters above the ellipsoid.
       * @param {Number} maximumHeight The highest height in the tile, in meters above the ellipsoid.
       * @param {BoundingSphere} boundingSphere3D A bounding sphere that completely contains the tile.
@@ -27,10 +29,19 @@ define([
       * @param {OrientedBoundingBox} [orientedBoundingBox] A bounding box that completely contains the tile.
       * @param {TerrainEncoding} encoding Information used to decode the mesh.
       * @param {Number} exaggeration The amount that this mesh was exaggerated.
+      * @param {Number[]} westIndicesSouthToNorth The indices of the vertices on the Western edge of the tile, ordered from South to North (clockwise).
+      * @param {Number[]} southIndicesEastToWest The indices of the vertices on the Southern edge of the tile, ordered from East to West (clockwise).
+      * @param {Number[]} eastIndicesNorthToSouth The indices of the vertices on the Eastern edge of the tile, ordered from North to South (clockwise).
+      * @param {Number[]} northIndicesWestToEast The indices of the vertices on the Northern edge of the tile, ordered from West to East (clockwise).
       *
       * @private
       */
-    function TerrainMesh(center, vertices, indices, minimumHeight, maximumHeight, boundingSphere3D, occludeePointInScaledSpace, vertexStride, orientedBoundingBox, encoding, exaggeration) {
+    function TerrainMesh(
+        center, vertices, indices, minimumHeight, maximumHeight,
+        boundingSphere3D, occludeePointInScaledSpace,
+        vertexStride, orientedBoundingBox, encoding, exaggeration,
+        westIndicesSouthToNorth, southIndicesEastToWest, eastIndicesNorthToSouth, northIndicesWestToEast) {
+
         /**
          * The center of the tile.  Vertex positions are specified relative to this center.
          * @type {Cartesian3}
@@ -57,7 +68,7 @@ define([
 
         /**
          * The indices describing how the vertices are connected to form triangles.
-         * @type {Uint16Array|Uint32Array}
+         * @type {Uint8Array|Uint16Array|Uint32Array}
          */
         this.indices = indices;
 
@@ -104,6 +115,30 @@ define([
          * @type {Number}
          */
         this.exaggeration = exaggeration;
+
+        /**
+         * The indices of the vertices on the Western edge of the tile, ordered from South to North (clockwise).
+         * @type {Number[]}
+         */
+        this.westIndicesSouthToNorth = westIndicesSouthToNorth;
+
+        /**
+         * The indices of the vertices on the Southern edge of the tile, ordered from East to West (clockwise).
+         * @type {Number[]}
+         */
+        this.southIndicesEastToWest = southIndicesEastToWest;
+
+        /**
+         * The indices of the vertices on the Eastern edge of the tile, ordered from North to South (clockwise).
+         * @type {Number[]}
+         */
+        this.eastIndicesNorthToSouth = eastIndicesNorthToSouth;
+
+        /**
+         * The indices of the vertices on the Northern edge of the tile, ordered from West to East (clockwise).
+         * @type {Number[]}
+         */
+        this.northIndicesWestToEast = northIndicesWestToEast;
     }
 
     return TerrainMesh;
diff --git a/Source/Core/TileEdge.js b/Source/Core/TileEdge.js
new file mode 100644
index 00000000000..d1d54ebe0b3
--- /dev/null
+++ b/Source/Core/TileEdge.js
@@ -0,0 +1,20 @@
+define([
+    ], function() {
+    'use strict';
+
+    /**
+     * @private
+     */
+    var TileEdge = {
+        WEST: 0,
+        NORTH: 1,
+        EAST: 2,
+        SOUTH: 3,
+        NORTHWEST: 4,
+        NORTHEAST: 5,
+        SOUTHWEST: 6,
+        SOUTHEAST: 7
+    };
+
+    return TileEdge;
+});
diff --git a/Source/DataSources/LabelVisualizer.js b/Source/DataSources/LabelVisualizer.js
index 94057552ade..ab63dacd866 100644
--- a/Source/DataSources/LabelVisualizer.js
+++ b/Source/DataSources/LabelVisualizer.js
@@ -136,10 +136,19 @@ define([
                 cluster._clusterDirty = true;
             }
 
+            var updateClamping = false;
+            var heightReference = Property.getValueOrDefault(labelGraphics._heightReference, time, defaultHeightReference);
+
             if (!defined(label)) {
                 label = cluster.getLabel(entity);
                 label.id = entity;
                 item.label = label;
+
+                // If this new label happens to have a position and height reference that match our new values,
+                // label._updateClamping will not be called automatically. That's a problem because the clamped
+                // height may be based on different terrain than is now loaded. So we'll manually call
+                // _updateClamping below.
+                updateClamping = Cartesian3.equals(label.position, position) && label.heightReference === heightReference;
             }
 
             label.show = true;
@@ -156,7 +165,7 @@ define([
             label.backgroundPadding = Property.getValueOrDefault(labelGraphics._backgroundPadding, time, defaultBackgroundPadding, backgroundPaddingScratch);
             label.pixelOffset = Property.getValueOrDefault(labelGraphics._pixelOffset, time, defaultPixelOffset, pixelOffsetScratch);
             label.eyeOffset = Property.getValueOrDefault(labelGraphics._eyeOffset, time, defaultEyeOffset, eyeOffsetScratch);
-            label.heightReference = Property.getValueOrDefault(labelGraphics._heightReference, time, defaultHeightReference);
+            label.heightReference = heightReference;
             label.horizontalOrigin = Property.getValueOrDefault(labelGraphics._horizontalOrigin, time, defaultHorizontalOrigin);
             label.verticalOrigin = Property.getValueOrDefault(labelGraphics._verticalOrigin, time, defaultVerticalOrigin);
             label.translucencyByDistance = Property.getValueOrUndefined(labelGraphics._translucencyByDistance, time, translucencyByDistanceScratch);
@@ -164,6 +173,10 @@ define([
             label.scaleByDistance = Property.getValueOrUndefined(labelGraphics._scaleByDistance, time, scaleByDistanceScratch);
             label.distanceDisplayCondition = Property.getValueOrUndefined(labelGraphics._distanceDisplayCondition, time, distanceDisplayConditionScratch);
             label.disableDepthTestDistance = Property.getValueOrUndefined(labelGraphics._disableDepthTestDistance, time);
+
+            if (updateClamping) {
+                label._updateClamping();
+            }
         }
         return true;
     };
diff --git a/Source/DataSources/PointVisualizer.js b/Source/DataSources/PointVisualizer.js
index d674c05e3f3..c2aa7a70245 100644
--- a/Source/DataSources/PointVisualizer.js
+++ b/Source/DataSources/PointVisualizer.js
@@ -114,6 +114,7 @@ define([
             }
 
             var needsRedraw = false;
+            var updateClamping = false;
             if ((heightReference !== HeightReference.NONE) && !defined(billboard)) {
                 if (defined(pointPrimitive)) {
                     returnPrimitive(item, entity, cluster);
@@ -125,6 +126,12 @@ define([
                 billboard.image = undefined;
                 item.billboard = billboard;
                 needsRedraw = true;
+
+                // If this new billboard happens to have a position and height reference that match our new values,
+                // billboard._updateClamping will not be called automatically. That's a problem because the clamped
+                // height may be based on different terrain than is now loaded. So we'll manually call
+                // _updateClamping below.
+                updateClamping = Cartesian3.equals(billboard.position, position) && billboard.heightReference === heightReference;
             } else if ((heightReference === HeightReference.NONE) && !defined(pointPrimitive)) {
                 if (defined(billboard)) {
                     returnPrimitive(item, entity, cluster);
@@ -190,6 +197,10 @@ define([
 
                     billboard.setImage(textureId, createBillboardPointCallback(centerAlpha, cssColor, cssOutlineColor, newOutlineWidth, newPixelSize));
                 }
+
+                if (updateClamping) {
+                    billboard._updateClamping();
+                }
             }
         }
         return true;
diff --git a/Source/Renderer/Texture.js b/Source/Renderer/Texture.js
index 46bde0baabf..ddb267bc446 100644
--- a/Source/Renderer/Texture.js
+++ b/Source/Renderer/Texture.js
@@ -267,6 +267,15 @@ define([
         this.sampler = defined(options.sampler) ? options.sampler : new Sampler();
     }
 
+    /**
+     * This function is identical to using the Texture constructor except that it can be
+     * replaced with a mock/spy in tests.
+     * @private
+     */
+    Texture.create = function(options) {
+        return new Texture(options);
+    };
+
     /**
      * Creates a texture, and copies a subimage of the framebuffer to it.  When called without arguments,
      * the texture is the same width and height as the framebuffer and contains its contents.
diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js
index 8ed9ef7cf8a..c2cbbc67e28 100644
--- a/Source/Scene/Globe.js
+++ b/Source/Scene/Globe.js
@@ -26,7 +26,8 @@ define([
         './ImageryLayerCollection',
         './QuadtreePrimitive',
         './SceneMode',
-        './ShadowMode'
+        './ShadowMode',
+        './TileSelectionResult'
     ], function(
         BoundingSphere,
         buildModuleUrl,
@@ -55,7 +56,8 @@ define([
         ImageryLayerCollection,
         QuadtreePrimitive,
         SceneMode,
-        ShadowMode) {
+        ShadowMode,
+        TileSelectionResult) {
     'use strict';
 
     /**
@@ -127,6 +129,48 @@ define([
          */
         this.tileCacheSize = 100;
 
+        /**
+         * Gets or sets the number of loading descendant tiles that is considered "too many".
+         * If a tile has too many loading descendants, that tile will be loaded and rendered before any of
+         * its descendants are loaded and rendered. This means more feedback for the user that something
+         * is happening at the cost of a longer overall load time. Setting this to 0 will cause each
+         * tile level to be loaded successively, significantly increasing load time. Setting it to a large
+         * number (e.g. 1000) will minimize the number of tiles that are loaded but tend to make
+         * detail appear all at once after a long wait.
+         * @type {Number}
+         * @default 20
+         */
+        this.loadingDescendantLimit = 20;
+
+        /**
+         * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded.
+         * Setting this to true optimizes the zoom-out experience and provides more detail in
+         * newly-exposed areas when panning. The down side is that it requires loading more tiles.
+         * @type {Boolean}
+         * @default true
+         */
+        this.preloadAncestors = true;
+
+        /**
+         * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded.
+         * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even
+         * if they are culled. Setting this to true may provide a better panning experience at the
+         * cost of loading more tiles.
+         * @type {Boolean}
+         * @default false
+         */
+        this.preloadSiblings = false;
+
+        /**
+         * The color to use to highlight terrain fill tiles. If undefined, fill tiles are not
+         * highlighted at all. The alpha value is used to alpha blend with the tile's
+         * actual color. Because terrain fill tiles do not represent the actual terrain surface,
+         * it may be useful in some applications to indicate visually that they are not to be trusted.
+         * @type {Color}
+         * @default undefined
+         */
+        this.fillHighlightColor = undefined;
+
         /**
          * Enable lighting the globe with the sun as a light source.
          *
@@ -275,18 +319,6 @@ define([
                 return this._surface.tileProvider.imageryLayersUpdatedEvent;
             }
         },
-        /**
-         * Gets an event that's raised when a surface tile is loaded and ready to be rendered.
-         *
-         * @memberof Globe.prototype
-         * @type {Event}
-         * @readonly
-         */
-        tileLoadedEvent : {
-            get : function() {
-                return this._surface.tileProvider.tileLoadedEvent;
-            }
-        },
         /**
          * Returns <code>true</code> when the tile load queue is empty, <code>false</code> otherwise.  When the load queue is empty,
          * all terrain and imagery for the current view have been loaded.
@@ -506,23 +538,26 @@ define([
 
         for (i = 0; i < length; ++i) {
             tile = tilesToRender[i];
-            var tileData = tile.data;
+            var surfaceTile = tile.data;
 
-            if (!defined(tileData)) {
+            if (!defined(surfaceTile)) {
                 continue;
             }
 
-            var boundingVolume = tileData.pickBoundingSphere;
+            var boundingVolume = surfaceTile.pickBoundingSphere;
             if (mode !== SceneMode.SCENE3D) {
-                BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, tileData.minimumHeight, tileData.maximumHeight, boundingVolume);
+                surfaceTile.pickBoundingSphere = boundingVolume = BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, surfaceTile.tileBoundingRegion.minimumHeight, surfaceTile.tileBoundingRegion.maximumHeight, boundingVolume);
                 Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);
+            } else if (defined(surfaceTile.renderedMesh)) {
+                BoundingSphere.clone(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume);
             } else {
-                BoundingSphere.clone(tileData.boundingSphere3D, boundingVolume);
+                // So wait how did we render this thing then? It shouldn't be possible to get here.
+                continue;
             }
 
             var boundingSphereIntersection = IntersectionTests.raySphere(ray, boundingVolume, scratchSphereIntersectionResult);
             if (defined(boundingSphereIntersection)) {
-                sphereIntersections.push(tileData);
+                sphereIntersections.push(surfaceTile);
             }
         }
 
@@ -603,22 +638,24 @@ define([
             }
         }
 
-        if (!defined(tile) || !Rectangle.contains(tile.rectangle, cartographic)) {
+        if (i >= length) {
             return undefined;
         }
 
-        while (tile.renderable) {
+        while (tile._lastSelectionResult === TileSelectionResult.REFINED) {
             tile = tileIfContainsCartographic(tile.southwestChild, cartographic) ||
                    tileIfContainsCartographic(tile.southeastChild, cartographic) ||
                    tileIfContainsCartographic(tile.northwestChild, cartographic) ||
                    tile.northeastChild;
         }
 
-        while (defined(tile) && (!defined(tile.data) || !defined(tile.data.pickTerrain))) {
-            tile = tile.parent;
-        }
-
-        if (!defined(tile)) {
+        // This tile was either rendered or culled.
+        // It is sometimes useful to get a height from a culled tile,
+        // e.g. when we're getting a height in order to place a billboard
+        // on terrain, and the camera is looking at that same billboard.
+        // The culled tile must have a valid mesh, though.
+        if (!defined(tile.data) || !defined(tile.data.renderedMesh)) {
+            // Tile was not rendered (culled).
             return undefined;
         }
 
@@ -638,7 +675,7 @@ define([
         if (!defined(rayOrigin)) {
             // intersection point is outside the ellipsoid, try other value
             // minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider
-            var magnitude = Math.min(defaultValue(tile.data.minimumHeight, 0.0),-11500.0);
+            var magnitude = Math.min(defaultValue(tile.data.minimumHeight, 0.0), -11500.0);
 
             // multiply by the *positive* value of the magnitude
             var vectorToMinimumPoint = Cartesian3.multiplyByScalar(surfaceNormal, Math.abs(magnitude) + 1, scratchGetHeightIntersection);
@@ -711,6 +748,9 @@ define([
 
             surface.maximumScreenSpaceError = this.maximumScreenSpaceError;
             surface.tileCacheSize = this.tileCacheSize;
+            surface.loadingDescendantLimit = this.loadingDescendantLimit;
+            surface.preloadAncestors = this.preloadAncestors;
+            surface.preloadSiblings = this.preloadSiblings;
 
             tileProvider.terrainProvider = this.terrainProvider;
             tileProvider.lightingFadeOutDistance = this.lightingFadeOutDistance;
@@ -726,6 +766,7 @@ define([
             tileProvider.hueShift = this.atmosphereHueShift;
             tileProvider.saturationShift = this.atmosphereSaturationShift;
             tileProvider.brightnessShift = this.atmosphereBrightnessShift;
+            tileProvider.fillHighlightColor = this.fillHighlightColor;
 
             surface.beginFrame(frameState);
         }
diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js
index ebf0d4e38ea..56429d4d573 100644
--- a/Source/Scene/GlobeSurfaceShaderSet.js
+++ b/Source/Scene/GlobeSurfaceShaderSet.js
@@ -90,11 +90,13 @@ define([
         var clippedByBoundaries = options.clippedByBoundaries;
         var hasImageryLayerCutout = options.hasImageryLayerCutout;
         var colorCorrect = options.colorCorrect;
+        var highlightFillTile = options.highlightFillTile;
 
         var quantization = 0;
         var quantizationDefine = '';
 
-        var terrainEncoding = surfaceTile.pickTerrain.mesh.encoding;
+        var mesh = surfaceTile.renderedMesh;
+        var terrainEncoding = mesh.encoding;
         var quantizationMode = terrainEncoding.quantization;
         if (quantizationMode === TerrainQuantization.BITS12) {
             quantization = 1;
@@ -103,7 +105,7 @@ define([
 
         var vertexLogDepth = 0;
         var vertexLogDepthDefine = '';
-        if (surfaceTile.terrainData._createdByUpsampling) {
+        if (!defined(surfaceTile.vertexArray) || !defined(surfaceTile.terrainData) || surfaceTile.terrainData._createdByUpsampling) {
             vertexLogDepth = 1;
             vertexLogDepthDefine = 'DISABLE_GL_POSITION_LOG_DEPTH';
         }
@@ -144,7 +146,8 @@ define([
                     (vertexLogDepth << 19) |
                     (cartographicLimitRectangleFlag << 20) |
                     (imageryCutoutFlag << 21) |
-                    (colorCorrect << 22);
+                    (colorCorrect << 22) |
+                    (highlightFillTile << 23);
 
         var currentClippingShaderState = 0;
         if (defined(clippingPlanes) && clippingPlanes.length > 0) {
@@ -243,6 +246,10 @@ define([
                 fs.defines.push('COLOR_CORRECT');
             }
 
+            if (highlightFillTile) {
+                fs.defines.push('HIGHLIGHT_FILL_TILE');
+            }
+
             var computeDayColor = '\
     vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates)\n\
     {\n\
diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js
index 579d73a1bf2..79c3a221b85 100644
--- a/Source/Scene/GlobeSurfaceTile.js
+++ b/Source/Scene/GlobeSurfaceTile.js
@@ -2,44 +2,58 @@ define([
         '../Core/BoundingSphere',
         '../Core/Cartesian3',
         '../Core/Cartesian4',
-        '../Core/defaultValue',
         '../Core/defined',
         '../Core/defineProperties',
+        '../Core/IndexDatatype',
         '../Core/IntersectionTests',
         '../Core/PixelFormat',
+        '../Core/Request',
+        '../Core/RequestState',
+        '../Core/RequestType',
+        '../Core/TileProviderError',
+        '../Renderer/Buffer',
+        '../Renderer/BufferUsage',
         '../Renderer/PixelDatatype',
         '../Renderer/Sampler',
         '../Renderer/Texture',
         '../Renderer/TextureMagnificationFilter',
         '../Renderer/TextureMinificationFilter',
         '../Renderer/TextureWrap',
+        '../Renderer/VertexArray',
         './ImageryState',
         './QuadtreeTileLoadState',
         './SceneMode',
         './TerrainState',
         './TileBoundingRegion',
-        './TileTerrain'
+        '../ThirdParty/when'
     ], function(
         BoundingSphere,
         Cartesian3,
         Cartesian4,
-        defaultValue,
         defined,
         defineProperties,
+        IndexDatatype,
         IntersectionTests,
         PixelFormat,
+        Request,
+        RequestState,
+        RequestType,
+        TileProviderError,
+        Buffer,
+        BufferUsage,
         PixelDatatype,
         Sampler,
         Texture,
         TextureMagnificationFilter,
         TextureMinificationFilter,
         TextureWrap,
+        VertexArray,
         ImageryState,
         QuadtreeTileLoadState,
         SceneMode,
         TerrainState,
         TileBoundingRegion,
-        TileTerrain) {
+        when) {
     'use strict';
 
     /**
@@ -59,25 +73,28 @@ define([
         this.imagery = [];
 
         this.waterMaskTexture = undefined;
-
         this.waterMaskTranslationAndScale = new Cartesian4(0.0, 0.0, 1.0, 1.0);
 
         this.terrainData = undefined;
-        this.center = new Cartesian3();
         this.vertexArray = undefined;
-        this.minimumHeight = 0.0;
-        this.maximumHeight = 0.0;
-        this.boundingSphere3D = new BoundingSphere();
-        this.boundingSphere2D = new BoundingSphere();
         this.orientedBoundingBox = undefined;
+        this.boundingVolumeSourceTile = undefined;
+
+        /**
+         * A bounding region used to estimate distance to the tile. The horizontal bounds are always tight-fitting,
+         * but the `minimumHeight` and `maximumHeight` properties may be derived from the min/max of an ancestor tile
+         * and be quite loose-fitting and thus very poor for estimating distance. The {@link TileBoundingRegion#boundingVolume}
+         * and {@link TileBoundingRegion#boundingSphere} will always be undefined; tiles store these separately.
+         * @type {TileBoundingRegion}
+         */
         this.tileBoundingRegion = undefined;
         this.occludeePointInScaledSpace = new Cartesian3();
 
-        this.loadedTerrain = undefined;
-        this.upsampledTerrain = undefined;
+        this.terrainState = TerrainState.UNLOADED;
+        this.mesh = undefined;
+        this.fill = undefined;
 
         this.pickBoundingSphere = new BoundingSphere();
-        this.pickTerrain = undefined;
 
         this.surfaceShader = undefined;
         this.isClipped = true;
@@ -99,15 +116,10 @@ define([
             get : function() {
                 // Do not remove tiles that are transitioning or that have
                 // imagery that is transitioning.
-                var loadedTerrain = this.loadedTerrain;
-                var loadingIsTransitioning = defined(loadedTerrain) &&
-                                             (loadedTerrain.state === TerrainState.RECEIVING || loadedTerrain.state === TerrainState.TRANSFORMING);
-
-                var upsampledTerrain = this.upsampledTerrain;
-                var upsamplingIsTransitioning = defined(upsampledTerrain) &&
-                                                (upsampledTerrain.state === TerrainState.RECEIVING || upsampledTerrain.state === TerrainState.TRANSFORMING);
+                var terrainState = this.terrainState;
+                var loadingIsTransitioning = terrainState === TerrainState.RECEIVING || terrainState === TerrainState.TRANSFORMING;
 
-                var shouldRemoveTile = !loadingIsTransitioning && !upsamplingIsTransitioning;
+                var shouldRemoveTile = !loadingIsTransitioning;
 
                 var imagery = this.imagery;
                 for (var i = 0, len = imagery.length; shouldRemoveTile && i < len; ++i) {
@@ -117,6 +129,27 @@ define([
 
                 return shouldRemoveTile;
             }
+        },
+
+        /**
+         * Gets the {@link TerrainMesh} that is used for rendering this tile, if any.
+         * Returns the value of the {@link GlobeSurfaceTile#mesh} property if
+         * {@link GlobeSurfaceTile#vertexArray} is defined. Otherwise, It returns the
+         * {@link TerrainFillMesh#mesh} property of the {@link GlobeSurfaceTile#fill}.
+         * If there is no fill, it returns undefined.
+         *
+         * @memberof GlobeSurfaceTile.prototype
+         * @type {TerrainMesh}
+         */
+        renderedMesh : {
+            get : function() {
+                if (defined(this.vertexArray)) {
+                    return this.mesh;
+                } else if (defined(this.fill)) {
+                    return this.fill.mesh;
+                }
+                return undefined;
+            }
         }
     });
 
@@ -139,12 +172,7 @@ define([
     var scratchResult = new Cartesian3();
 
     GlobeSurfaceTile.prototype.pick = function(ray, mode, projection, cullBackFaces, result) {
-        var terrain = this.pickTerrain;
-        if (!defined(terrain)) {
-            return undefined;
-        }
-
-        var mesh = terrain.mesh;
+        var mesh = this.renderedMesh;
         if (!defined(mesh)) {
             return undefined;
         }
@@ -183,25 +211,12 @@ define([
 
         this.terrainData = undefined;
 
-        if (defined(this.loadedTerrain)) {
-            this.loadedTerrain.freeResources();
-            this.loadedTerrain = undefined;
-        }
-
-        if (defined(this.upsampledTerrain)) {
-            this.upsampledTerrain.freeResources();
-            this.upsampledTerrain = undefined;
-        }
-
-        if (defined(this.pickTerrain)) {
-            this.pickTerrain.freeResources();
-            this.pickTerrain = undefined;
-        }
-
-        var i, len;
+        this.terrainState = TerrainState.UNLOADED;
+        this.mesh = undefined;
+        this.fill = this.fill && this.fill.destroy();
 
         var imageryList = this.imagery;
-        for (i = 0, len = imageryList.length; i < len; ++i) {
+        for (var i = 0, len = imageryList.length; i < len; ++i) {
             imageryList[i].freeResources();
         }
         this.imagery.length = 0;
@@ -210,88 +225,85 @@ define([
     };
 
     GlobeSurfaceTile.prototype.freeVertexArray = function() {
-        var indexBuffer;
-
-        if (defined(this.vertexArray)) {
-            indexBuffer = this.vertexArray.indexBuffer;
-
-            this.vertexArray = this.vertexArray.destroy();
-
-            if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
-                --indexBuffer.referenceCount;
-                if (indexBuffer.referenceCount === 0) {
-                    indexBuffer.destroy();
-                }
-            }
-        }
-
-        if (defined(this.wireframeVertexArray)) {
-            indexBuffer = this.wireframeVertexArray.indexBuffer;
-
-            this.wireframeVertexArray = this.wireframeVertexArray.destroy();
-
-            if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
-                --indexBuffer.referenceCount;
-                if (indexBuffer.referenceCount === 0) {
-                    indexBuffer.destroy();
-                }
-            }
-        }
+        GlobeSurfaceTile._freeVertexArray(this.vertexArray);
+        this.vertexArray = undefined;
+        GlobeSurfaceTile._freeVertexArray(this.wireframeVertexArray);
+        this.wireframeVertexArray = undefined;
     };
 
-    function createTileBoundingRegion(tile) {
-        var minimumHeight;
-        var maximumHeight;
-        if (defined(tile.parent) && defined(tile.parent.data)) {
-            minimumHeight = tile.parent.data.minimumHeight;
-            maximumHeight = tile.parent.data.maximumHeight;
-        }
-        return new TileBoundingRegion({
-            rectangle : tile.rectangle,
-            ellipsoid : tile.tilingScheme.ellipsoid,
-            minimumHeight : minimumHeight,
-            maximumHeight : maximumHeight
-        });
-    }
-
-    function createPriorityFunction(surfaceTile, frameState) {
-        return function() {
-            return surfaceTile.tileBoundingRegion.distanceToCamera(frameState);
-        };
-    }
-
-    GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) {
+    GlobeSurfaceTile.initialize = function(tile, terrainProvider, imageryLayerCollection) {
         var surfaceTile = tile.data;
         if (!defined(surfaceTile)) {
             surfaceTile = tile.data = new GlobeSurfaceTile();
-            // Create the TileBoundingRegion now in order to estimate the distance, which is used to prioritize the request.
-            // Since the terrain isn't loaded yet, estimate the heights using its parent's values.
-            surfaceTile.tileBoundingRegion = createTileBoundingRegion(tile);
-        }
-
-        if (!defined(tile._priorityFunction)) {
-            // The priority function is used to prioritize requests among all requested tiles
-            tile._priorityFunction = createPriorityFunction(surfaceTile, frameState);
         }
 
         if (tile.state === QuadtreeTileLoadState.START) {
             prepareNewTile(tile, terrainProvider, imageryLayerCollection);
             tile.state = QuadtreeTileLoadState.LOADING;
         }
+    };
+
+    GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy, terrainOnly) {
+        GlobeSurfaceTile.initialize(tile, terrainProvider, imageryLayerCollection);
+
+        var surfaceTile = tile.data;
 
         if (tile.state === QuadtreeTileLoadState.LOADING) {
-            processTerrainStateMachine(tile, frameState, terrainProvider, vertexArraysToDestroy);
+            processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy);
         }
 
+        // From here down we're loading imagery, not terrain. We don't want to load imagery until
+        // we're certain that the terrain tiles are actually visible, though. We'll load terrainOnly
+        // in these scenarios:
+        //   * our bounding volume isn't accurate so we're not certain this tile is really visible (see GlobeSurfaceTileProvider#loadTile).
+        //   * we want to upsample from this tile but don't plan to render it (see processTerrainStateMachine).
+        if (terrainOnly) {
+            return;
+        }
+
+        var wasAlreadyRenderable = tile.renderable;
+
         // The terrain is renderable as soon as we have a valid vertex array.
-        var isRenderable = defined(surfaceTile.vertexArray);
+        tile.renderable = defined(surfaceTile.vertexArray);
 
-        // But it's not done loading until our two state machines are terminated.
-        var isDoneLoading = !defined(surfaceTile.loadedTerrain) && !defined(surfaceTile.upsampledTerrain);
+        // But it's not done loading until it's in the READY state.
+        var isTerrainDoneLoading = surfaceTile.terrainState === TerrainState.READY;
 
         // If this tile's terrain and imagery are just upsampled from its parent, mark the tile as
         // upsampled only.  We won't refine a tile if its four children are upsampled only.
-        var isUpsampledOnly = defined(surfaceTile.terrainData) && surfaceTile.terrainData.wasCreatedByUpsampling();
+        tile.upsampledFromParent = defined(surfaceTile.terrainData) && surfaceTile.terrainData.wasCreatedByUpsampling();
+
+        var isImageryDoneLoading = surfaceTile.processImagery(tile, terrainProvider, frameState);
+
+        if (isTerrainDoneLoading && isImageryDoneLoading) {
+            var callbacks = tile._loadedCallbacks;
+            var newCallbacks = {};
+            for(var layerId in callbacks) {
+                if (callbacks.hasOwnProperty(layerId)) {
+                    if(!callbacks[layerId](tile)) {
+                        newCallbacks[layerId] = callbacks[layerId];
+                    }
+                }
+            }
+            tile._loadedCallbacks = newCallbacks;
+
+            tile.state = QuadtreeTileLoadState.DONE;
+        }
+
+        // Once a tile is renderable, it stays renderable, because doing otherwise would
+        // cause detail (or maybe even the entire globe) to vanish when adding a new
+        // imagery layer. `GlobeSurfaceTileProvider._onLayerAdded` sets renderable to
+        // false for all affected tiles that are not currently being rendered.
+        if (wasAlreadyRenderable) {
+            tile.renderable = true;
+        }
+    };
+
+    GlobeSurfaceTile.prototype.processImagery = function(tile, terrainProvider, frameState, skipLoading) {
+        var surfaceTile = tile.data;
+        var isUpsampledOnly = tile.upsampledFromParent;
+        var isRenderable = tile.renderable;
+        var isDoneLoading = true;
 
         // Transition imagery states
         var tileImageryCollection = surfaceTile.imagery;
@@ -319,7 +331,7 @@ define([
                 }
             }
 
-            var thisTileDoneLoading = tileImagery.processStateMachine(tile, frameState);
+            var thisTileDoneLoading = tileImagery.processStateMachine(tile, frameState, skipLoading);
             isDoneLoading = isDoneLoading && thisTileDoneLoading;
 
             // The imagery is renderable as soon as we have any renderable imagery for this region.
@@ -330,41 +342,15 @@ define([
         }
 
         tile.upsampledFromParent = isUpsampledOnly;
+        tile.renderable = isRenderable;
 
-        // The tile becomes renderable when the terrain and all imagery data are loaded.
-        if (i === len) {
-            if (isRenderable) {
-                tile.renderable = true;
-            }
-
-            if (isDoneLoading) {
-                var callbacks = tile._loadedCallbacks;
-                var newCallbacks = {};
-                for(var layerId in callbacks) {
-                    if (callbacks.hasOwnProperty(layerId)) {
-                        if(!callbacks[layerId](tile)) {
-                            newCallbacks[layerId] = callbacks[layerId];
-                        }
-                    }
-                }
-                tile._loadedCallbacks = newCallbacks;
-
-                tile.state = QuadtreeTileLoadState.DONE;
-                tile._priorityFunction = undefined;
-            }
-        }
+        return isDoneLoading;
     };
 
     function prepareNewTile(tile, terrainProvider, imageryLayerCollection) {
-        var surfaceTile = tile.data;
-
-        var upsampleTileDetails = getUpsampleTileDetails(tile);
-        if (defined(upsampleTileDetails)) {
-            surfaceTile.upsampledTerrain = new TileTerrain(upsampleTileDetails);
-        }
-
-        if (isDataAvailable(tile, terrainProvider)) {
-            surfaceTile.loadedTerrain = new TileTerrain();
+        if (terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level) === false) {
+            // This tile is not available, so mark it failed so we start upsampling right away.
+            tile.data.terrainState = TerrainState.FAILED;
         }
 
         // Map imagery tiles to this terrain tile
@@ -376,229 +362,222 @@ define([
         }
     }
 
-    function processTerrainStateMachine(tile, frameState, terrainProvider, vertexArraysToDestroy) {
+    function processTerrainStateMachine(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) {
         var surfaceTile = tile.data;
-        var loaded = surfaceTile.loadedTerrain;
-        var upsampled = surfaceTile.upsampledTerrain;
-        var suspendUpsampling = false;
-
-        if (defined(loaded)) {
-            loaded.processLoadStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction);
-
-            // Publish the terrain data on the tile as soon as it is available.
-            // We'll potentially need it to upsample child tiles.
-            if (loaded.state >= TerrainState.RECEIVED) {
-                if (surfaceTile.terrainData !== loaded.data) {
-                    surfaceTile.terrainData = loaded.data;
-
-                    // If there's a water mask included in the terrain data, create a
-                    // texture for it.
-                    createWaterMaskTextureIfNeeded(frameState.context, surfaceTile);
 
-                    propagateNewLoadedDataToChildren(tile);
-                }
-                suspendUpsampling = true;
-            }
-
-            if (loaded.state === TerrainState.READY) {
-                loaded.publishToTile(tile);
-
-                if (defined(tile.data.vertexArray)) {
-                    // Free the tiles existing vertex array on next render.
-                    vertexArraysToDestroy.push(tile.data.vertexArray);
-                }
-
-                // Transfer ownership of the vertex array to the tile itself.
-                tile.data.vertexArray = loaded.vertexArray;
-                loaded.vertexArray = undefined;
-
-                // No further loading or upsampling is necessary.
-                surfaceTile.pickTerrain = defaultValue(surfaceTile.loadedTerrain, surfaceTile.upsampledTerrain);
-                surfaceTile.loadedTerrain = undefined;
-                surfaceTile.upsampledTerrain = undefined;
-            } else if (loaded.state === TerrainState.FAILED) {
-                // Loading failed for some reason, or data is simply not available,
-                // so no need to continue trying to load.  Any retrying will happen before we
-                // reach this point.
-                surfaceTile.loadedTerrain = undefined;
+        // If this tile is FAILED, we'll need to upsample from the parent. If the parent isn't
+        // ready for that, let's push it along.
+        var parent = tile.parent;
+        if (surfaceTile.terrainState === TerrainState.FAILED && parent !== undefined) {
+            var parentReady = parent.data !== undefined && parent.data.terrainData !== undefined && parent.data.terrainData.canUpsample !== false;
+            if (!parentReady) {
+                GlobeSurfaceTile.processStateMachine(parent, frameState, terrainProvider, imageryLayerCollection, true);
             }
         }
 
-        if (!suspendUpsampling && defined(upsampled)) {
-            upsampled.processUpsampleStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level);
-
-            // Publish the terrain data on the tile as soon as it is available.
-            // We'll potentially need it to upsample child tiles.
-            // It's safe to overwrite terrainData because we won't get here after
-            // loaded terrain data has been received.
-            if (upsampled.state >= TerrainState.RECEIVED) {
-                if (surfaceTile.terrainData !== upsampled.data) {
-                    surfaceTile.terrainData = upsampled.data;
+        if (surfaceTile.terrainState === TerrainState.FAILED) {
+            upsample(surfaceTile, tile, frameState, terrainProvider, tile.x, tile.y, tile.level);
+        }
 
-                    // If the terrain provider has a water mask, "upsample" that as well
-                    // by computing texture translation and scale.
-                    if (terrainProvider.hasWaterMask) {
-                        upsampleWaterMask(tile);
-                    }
+        if (surfaceTile.terrainState === TerrainState.UNLOADED) {
+            requestTileGeometry(surfaceTile, terrainProvider, tile.x, tile.y, tile.level);
+        }
 
-                    propagateNewUpsampledDataToChildren(tile);
-                }
-            }
+        if (surfaceTile.terrainState === TerrainState.RECEIVED) {
+            transform(surfaceTile, frameState, terrainProvider, tile.x, tile.y, tile.level);
+        }
 
-            if (upsampled.state === TerrainState.READY) {
-                upsampled.publishToTile(tile);
+        if (surfaceTile.terrainState === TerrainState.TRANSFORMED) {
+            createResources(surfaceTile, frameState.context, terrainProvider, tile.x, tile.y, tile.level, vertexArraysToDestroy);
+        }
 
-                if (defined(tile.data.vertexArray)) {
-                    // Free the tiles existing vertex array on next render.
-                    vertexArraysToDestroy.push(tile.data.vertexArray);
+        if (surfaceTile.terrainState >= TerrainState.RECEIVED && surfaceTile.waterMaskTexture === undefined && terrainProvider.hasWaterMask) {
+            var terrainData = surfaceTile.terrainData;
+            if (terrainData.waterMask !== undefined) {
+                createWaterMaskTextureIfNeeded(frameState.context, surfaceTile);
+            } else {
+                var sourceTile = surfaceTile._findAncestorTileWithTerrainData(tile);
+                if (defined(sourceTile) && defined(sourceTile.data.waterMaskTexture)) {
+                    surfaceTile.waterMaskTexture = sourceTile.data.waterMaskTexture;
+                    ++surfaceTile.waterMaskTexture.referenceCount;
+                    surfaceTile._computeWaterMaskTranslationAndScale(tile, sourceTile, surfaceTile.waterMaskTranslationAndScale);
                 }
-
-                // Transfer ownership of the vertex array to the tile itself.
-                tile.data.vertexArray = upsampled.vertexArray;
-                upsampled.vertexArray = undefined;
-
-                // No further upsampling is necessary.  We need to continue loading, though.
-                surfaceTile.pickTerrain = surfaceTile.upsampledTerrain;
-                surfaceTile.upsampledTerrain = undefined;
-            } else if (upsampled.state === TerrainState.FAILED) {
-                // Upsampling failed for some reason.  This is pretty much a catastrophic failure,
-                // but maybe we'll be saved by loading.
-                surfaceTile.upsampledTerrain = undefined;
             }
         }
     }
 
-    function getUpsampleTileDetails(tile) {
-        // Find the nearest ancestor with loaded terrain.
-        var sourceTile = tile.parent;
-        while (defined(sourceTile) && defined(sourceTile.data) && !defined(sourceTile.data.terrainData)) {
-            sourceTile = sourceTile.parent;
+    function upsample(surfaceTile, tile, frameState, terrainProvider, x, y, level) {
+        var parent = tile.parent;
+        if (!parent) {
+            // Trying to upsample from a root tile. No can do. This tile is a failure.
+            tile.state = QuadtreeTileLoadState.FAILED;
+            return;
         }
 
-        if (!defined(sourceTile) || !defined(sourceTile.data)) {
-            // No ancestors have loaded terrain - try again later.
-            return undefined;
-        }
+        var sourceData = parent.data.terrainData;
+        var sourceX = parent.x;
+        var sourceY = parent.y;
+        var sourceLevel = parent.level;
 
-        return {
-            data : sourceTile.data.terrainData,
-            x : sourceTile.x,
-            y : sourceTile.y,
-            level : sourceTile.level
-        };
-    }
+        if (!defined(sourceData)) {
+            // Parent is not available, so we can't upsample this tile yet.
+            return;
+        }
 
-    function propagateNewUpsampledDataToChildren(tile) {
-        // Now that there's new data for this tile:
-        //  - child tiles that were previously upsampled need to be re-upsampled based on the new data.
+        var terrainDataPromise = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level);
+        if (!defined(terrainDataPromise)) {
+            // The upsample request has been deferred - try again later.
+            return;
+        }
 
-        // Generally this is only necessary when a child tile is upsampled, and then one
-        // of its ancestors receives new (better) data and we want to re-upsample from the
-        // new data.
+        surfaceTile.terrainState = TerrainState.RECEIVING;
 
-        propagateNewUpsampledDataToChild(tile, tile._southwestChild);
-        propagateNewUpsampledDataToChild(tile, tile._southeastChild);
-        propagateNewUpsampledDataToChild(tile, tile._northwestChild);
-        propagateNewUpsampledDataToChild(tile, tile._northeastChild);
+        when(terrainDataPromise, function(terrainData) {
+            surfaceTile.terrainData = terrainData;
+            surfaceTile.terrainState = TerrainState.RECEIVED;
+        }, function() {
+            surfaceTile.terrainState = TerrainState.FAILED;
+        });
     }
 
-    function propagateNewUpsampledDataToChild(tile, childTile) {
-        if (defined(childTile) && childTile.state !== QuadtreeTileLoadState.START) {
-            var childSurfaceTile = childTile.data;
-            if (defined(childSurfaceTile.terrainData) && !childSurfaceTile.terrainData.wasCreatedByUpsampling()) {
-                // Data for the child tile has already been loaded.
+    function requestTileGeometry(surfaceTile, terrainProvider, x, y, level) {
+        function success(terrainData) {
+            surfaceTile.terrainData = terrainData;
+            surfaceTile.terrainState = TerrainState.RECEIVED;
+            surfaceTile.request = undefined;
+        }
+
+        function failure() {
+            if (surfaceTile.request.state === RequestState.CANCELLED) {
+                // Cancelled due to low priority - try again later.
+                surfaceTile.terrainData = undefined;
+                surfaceTile.terrainState = TerrainState.UNLOADED;
+                surfaceTile.request = undefined;
                 return;
             }
 
-            // Restart the upsampling process, no matter its current state.
-            // We create a new instance rather than just restarting the existing one
-            // because there could be an asynchronous operation pending on the existing one.
-            if (defined(childSurfaceTile.upsampledTerrain)) {
-                childSurfaceTile.upsampledTerrain.freeResources();
-            }
-            childSurfaceTile.upsampledTerrain = new TileTerrain({
-                data : tile.data.terrainData,
-                x : tile.x,
-                y : tile.y,
-                level : tile.level
+            // Initially assume failure.  handleError may retry, in which case the state will
+            // change to RECEIVING or UNLOADED.
+            surfaceTile.terrainState = TerrainState.FAILED;
+            surfaceTile.request = undefined;
+
+            var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.';
+            terrainProvider._requestError = TileProviderError.handleError(
+                terrainProvider._requestError,
+                terrainProvider,
+                terrainProvider.errorEvent,
+                message,
+                x, y, level,
+                doRequest);
+        }
+
+        function doRequest() {
+            // Request the terrain from the terrain provider.
+            var request = new Request({
+                throttle : false,
+                throttleByServer : true,
+                type : RequestType.TERRAIN
             });
-
-            childTile.state = QuadtreeTileLoadState.LOADING;
+            surfaceTile.request = request;
+            var requestPromise = terrainProvider.requestTileGeometry(x, y, level, request);
+
+            // If the request method returns undefined (instead of a promise), the request
+            // has been deferred.
+            if (defined(requestPromise)) {
+                surfaceTile.terrainState = TerrainState.RECEIVING;
+                when(requestPromise, success, failure);
+            } else {
+                // Deferred - try again later.
+                surfaceTile.terrainState = TerrainState.UNLOADED;
+                surfaceTile.request = undefined;
+            }
         }
+
+        doRequest();
     }
 
-    function propagateNewLoadedDataToChildren(tile) {
-        var surfaceTile = tile.data;
+    function transform(surfaceTile, frameState, terrainProvider, x, y, level) {
+        var tilingScheme = terrainProvider.tilingScheme;
+
+        var terrainData = surfaceTile.terrainData;
+        var meshPromise = terrainData.createMesh(tilingScheme, x, y, level, frameState.terrainExaggeration);
+
+        if (!defined(meshPromise)) {
+            // Postponed.
+            return;
+        }
 
-        // Now that there's new data for this tile:
-        //  - child tiles that were previously upsampled need to be re-upsampled based on the new data.
-        //  - child tiles that were previously deemed unavailable may now be available.
+        surfaceTile.terrainState = TerrainState.TRANSFORMING;
 
-        propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.southwestChild);
-        propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.southeastChild);
-        propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.northwestChild);
-        propagateNewLoadedDataToChildTile(tile, surfaceTile, tile.northeastChild);
+        when(meshPromise, function(mesh) {
+            surfaceTile.mesh = mesh;
+            surfaceTile.terrainState = TerrainState.TRANSFORMED;
+        }, function() {
+            surfaceTile.terrainState = TerrainState.FAILED;
+        });
     }
 
-    function propagateNewLoadedDataToChildTile(tile, surfaceTile, childTile) {
-        if (childTile.state !== QuadtreeTileLoadState.START) {
-            var childSurfaceTile = childTile.data;
-            if (defined(childSurfaceTile.terrainData) && !childSurfaceTile.terrainData.wasCreatedByUpsampling()) {
-                // Data for the child tile has already been loaded.
-                return;
-            }
+    GlobeSurfaceTile._createVertexArrayForMesh = function(context, mesh) {
+        var typedArray = mesh.vertices;
+        var buffer = Buffer.createVertexBuffer({
+            context : context,
+            typedArray : typedArray,
+            usage : BufferUsage.STATIC_DRAW
+        });
+        var attributes = mesh.encoding.getAttributes(buffer);
 
-            // Restart the upsampling process, no matter its current state.
-            // We create a new instance rather than just restarting the existing one
-            // because there could be an asynchronous operation pending on the existing one.
-            if (defined(childSurfaceTile.upsampledTerrain)) {
-                childSurfaceTile.upsampledTerrain.freeResources();
-            }
-            childSurfaceTile.upsampledTerrain = new TileTerrain({
-                data : surfaceTile.terrainData,
-                x : tile.x,
-                y : tile.y,
-                level : tile.level
+        var indexBuffers = mesh.indices.indexBuffers || {};
+        var indexBuffer = indexBuffers[context.id];
+        if (!defined(indexBuffer) || indexBuffer.isDestroyed()) {
+            var indices = mesh.indices;
+            indexBuffer = Buffer.createIndexBuffer({
+                context : context,
+                typedArray : indices,
+                usage : BufferUsage.STATIC_DRAW,
+                indexDatatype : IndexDatatype.fromSizeInBytes(indices.BYTES_PER_ELEMENT)
             });
+            indexBuffer.vertexArrayDestroyable = false;
+            indexBuffer.referenceCount = 1;
+            indexBuffers[context.id] = indexBuffer;
+            mesh.indices.indexBuffers = indexBuffers;
+        } else {
+            ++indexBuffer.referenceCount;
+        }
 
-            if (surfaceTile.terrainData.isChildAvailable(tile.x, tile.y, childTile.x, childTile.y)) {
-                // Data is available for the child now.  It might have been before, too.
-                if (!defined(childSurfaceTile.loadedTerrain)) {
-                    // No load process is in progress, so start one.
-                    childSurfaceTile.loadedTerrain = new TileTerrain();
-                }
-            }
+        return new VertexArray({
+            context : context,
+            attributes : attributes,
+            indexBuffer : indexBuffer
+        });
+    };
 
-            childTile.state = QuadtreeTileLoadState.LOADING;
-        }
-    }
+    GlobeSurfaceTile._freeVertexArray = function(vertexArray) {
+        if (defined(vertexArray)) {
+            var indexBuffer = vertexArray.indexBuffer;
 
-    function isDataAvailable(tile, terrainProvider) {
-        var tileDataAvailable = terrainProvider.getTileDataAvailable(tile.x, tile.y, tile.level);
-        if (defined(tileDataAvailable)) {
-            return tileDataAvailable;
-        }
+            vertexArray.destroy();
 
-        var parent = tile.parent;
-        if (!defined(parent)) {
-            // Data is assumed to be available for root tiles.
-            return true;
+            if (defined(indexBuffer) && !indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
+                --indexBuffer.referenceCount;
+                if (indexBuffer.referenceCount === 0) {
+                    indexBuffer.destroy();
+                }
+            }
         }
 
-        if (!defined(parent.data) || !defined(parent.data.terrainData)) {
-            // Parent tile data is not yet received or upsampled, so assume (for now) that this
-            // child tile is not available.
-            return false;
-        }
+    };
 
-        return parent.data.terrainData.isChildAvailable(parent.x, parent.y, tile.x, tile.y);
+    function createResources(surfaceTile, context, terrainProvider, x, y, level, vertexArraysToDestroy) {
+        surfaceTile.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, surfaceTile.mesh);
+        surfaceTile.terrainState = TerrainState.READY;
+        surfaceTile.fill = surfaceTile.fill && surfaceTile.fill.destroy(vertexArraysToDestroy);
     }
 
     function getContextWaterMaskData(context) {
         var data = context.cache.tile_waterMaskData;
 
         if (!defined(data)) {
-            var allWaterTexture = new Texture({
+            var allWaterTexture = Texture.create({
                 context : context,
                 pixelFormat : PixelFormat.LUMINANCE,
                 pixelDatatype : PixelDatatype.UNSIGNED_BYTE,
@@ -632,20 +611,7 @@ define([
     }
 
     function createWaterMaskTextureIfNeeded(context, surfaceTile) {
-        var previousTexture = surfaceTile.waterMaskTexture;
-        if (defined(previousTexture)) {
-            --previousTexture.referenceCount;
-            if (previousTexture.referenceCount === 0) {
-                previousTexture.destroy();
-            }
-            surfaceTile.waterMaskTexture = undefined;
-        }
-
         var waterMask = surfaceTile.terrainData.waterMask;
-        if (!defined(waterMask)) {
-            return;
-        }
-
         var waterMaskData = getContextWaterMaskData(context);
         var texture;
 
@@ -661,7 +627,7 @@ define([
             }
         } else {
             var textureSize = Math.sqrt(waterMaskLength);
-            texture = new Texture({
+            texture = Texture.create({
                 context : context,
                 pixelFormat : PixelFormat.LUMINANCE,
                 pixelDatatype : PixelDatatype.UNSIGNED_BYTE,
@@ -683,24 +649,17 @@ define([
         Cartesian4.fromElements(0.0, 0.0, 1.0, 1.0, surfaceTile.waterMaskTranslationAndScale);
     }
 
-    function upsampleWaterMask(tile) {
-        var surfaceTile = tile.data;
-
-        // Find the nearest ancestor with loaded terrain.
+    GlobeSurfaceTile.prototype._findAncestorTileWithTerrainData = function(tile) {
         var sourceTile = tile.parent;
-        while (defined(sourceTile) && !defined(sourceTile.data.terrainData) || sourceTile.data.terrainData.wasCreatedByUpsampling()) {
-            sourceTile = sourceTile.parent;
-        }
 
-        if (!defined(sourceTile) || !defined(sourceTile.data.waterMaskTexture)) {
-            // No ancestors have a water mask texture - try again later.
-            return;
+        while (defined(sourceTile) && (!defined(sourceTile.data) || !defined(sourceTile.data.terrainData) || sourceTile.data.terrainData.wasCreatedByUpsampling())) {
+            sourceTile = sourceTile.parent;
         }
 
-        surfaceTile.waterMaskTexture = sourceTile.data.waterMaskTexture;
-        ++surfaceTile.waterMaskTexture.referenceCount;
+        return sourceTile;
+    };
 
-        // Compute the water mask translation and scale
+    GlobeSurfaceTile.prototype._computeWaterMaskTranslationAndScale = function(tile, sourceTile, result) {
         var sourceTileRectangle = sourceTile.rectangle;
         var tileRectangle = tile.rectangle;
         var tileWidth = tileRectangle.width;
@@ -708,11 +667,13 @@ define([
 
         var scaleX = tileWidth / sourceTileRectangle.width;
         var scaleY = tileHeight / sourceTileRectangle.height;
-        surfaceTile.waterMaskTranslationAndScale.x = scaleX * (tileRectangle.west - sourceTileRectangle.west) / tileWidth;
-        surfaceTile.waterMaskTranslationAndScale.y = scaleY * (tileRectangle.south - sourceTileRectangle.south) / tileHeight;
-        surfaceTile.waterMaskTranslationAndScale.z = scaleX;
-        surfaceTile.waterMaskTranslationAndScale.w = scaleY;
-    }
+        result.x = scaleX * (tileRectangle.west - sourceTileRectangle.west) / tileWidth;
+        result.y = scaleY * (tileRectangle.south - sourceTileRectangle.south) / tileHeight;
+        result.z = scaleX;
+        result.w = scaleY;
+
+        return result;
+    };
 
     return GlobeSurfaceTile;
 });
diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js
index 29b0411c7d8..2f599e2b4ad 100644
--- a/Source/Scene/GlobeSurfaceTileProvider.js
+++ b/Source/Scene/GlobeSurfaceTileProvider.js
@@ -25,6 +25,7 @@ define([
         '../Core/PrimitiveType',
         '../Core/Rectangle',
         '../Core/SphereOutlineGeometry',
+        '../Core/TerrainMesh',
         '../Core/TerrainQuantization',
         '../Core/Visibility',
         '../Core/WebMercatorProjection',
@@ -36,6 +37,9 @@ define([
         '../Renderer/RenderState',
         '../Renderer/VertexArray',
         './BlendingState',
+        './ImageryState',
+        './TileBoundingRegion',
+        './TileSelectionResult',
         './ClippingPlaneCollection',
         './DepthFunction',
         './GlobeSurfaceTile',
@@ -44,7 +48,9 @@ define([
         './Primitive',
         './QuadtreeTileLoadState',
         './SceneMode',
-        './ShadowMode'
+        './ShadowMode',
+        './TerrainFillMesh',
+        './TerrainState'
     ], function(
         BoundingSphere,
         BoxOutlineGeometry,
@@ -72,6 +78,7 @@ define([
         PrimitiveType,
         Rectangle,
         SphereOutlineGeometry,
+        TerrainMesh,
         TerrainQuantization,
         Visibility,
         WebMercatorProjection,
@@ -83,6 +90,9 @@ define([
         RenderState,
         VertexArray,
         BlendingState,
+        ImageryState,
+        TileBoundingRegion,
+        TileSelectionResult,
         ClippingPlaneCollection,
         DepthFunction,
         GlobeSurfaceTile,
@@ -91,7 +101,9 @@ define([
         Primitive,
         QuadtreeTileLoadState,
         SceneMode,
-        ShadowMode) {
+        ShadowMode,
+        TerrainFillMesh,
+        TerrainState) {
     'use strict';
 
     /**
@@ -130,9 +142,19 @@ define([
         this.showGroundAtmosphere = false;
         this.shadows = ShadowMode.RECEIVE_ONLY;
 
-        this.hueShift = undefined;
-        this.saturationShift = undefined;
-        this.brightnessShift = undefined;
+        /**
+         * The color to use to highlight terrain fill tiles. If undefined, fill tiles are not
+         * highlighted at all. The alpha value is used to alpha blend with the tile's
+         * actual color. Because terrain fill tiles do not represent the actual terrain surface,
+         * it may be useful in some applications to indicate visually that they are not to be trusted.
+         * @type {Color}
+         * @default undefined
+         */
+        this.fillHighlightColor = undefined;
+
+        this.hueShift = 0.0;
+        this.saturationShift = 0.0;
+        this.brightnessShift = 0.0;
 
         this._quadtree = undefined;
         this._terrainProvider = options.terrainProvider;
@@ -148,7 +170,6 @@ define([
         this._imageryLayers.layerRemoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerRemoved, this);
         this._imageryLayers.layerMoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerMoved, this);
         this._imageryLayers.layerShownOrHidden.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden, this);
-        this._tileLoadedEvent = new Event();
         this._imageryLayersUpdatedEvent = new Event();
 
         this._layerOrderChanged = false;
@@ -181,6 +202,9 @@ define([
          * @type {Rectangle}
          */
         this.cartographicLimitRectangle = Rectangle.clone(Rectangle.MAX_VALUE);
+
+        this._hasLoadedTilesThisFrame = false;
+        this._hasFillTilesThisFrame = false;
     }
 
     defineProperties(GlobeSurfaceTileProvider.prototype, {
@@ -262,17 +286,6 @@ define([
             }
         },
 
-        /**
-         * Gets an event that is raised when an globe surface tile is loaded and ready to be rendered.
-         * @memberof GlobeSurfaceTileProvider.prototype
-         * @type {Event}
-         */
-        tileLoadedEvent : {
-            get : function() {
-                return this._tileLoadedEvent;
-            }
-        },
-
         /**
          * Gets an event that is raised when an imagery layer is added, shown, hidden, moved, or removed.
          * @memberof GlobeSurfaceTileProvider.prototype
@@ -350,18 +363,6 @@ define([
         this._imageryLayers._update();
     };
 
-    function freeVertexArray(vertexArray) {
-        var indexBuffer = vertexArray.indexBuffer;
-        vertexArray.destroy();
-
-        if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
-            --indexBuffer.referenceCount;
-            if (indexBuffer.referenceCount === 0) {
-                indexBuffer.destroy();
-            }
-        }
-    }
-
     function updateCredits(surface, frameState) {
         var creditDisplay = frameState.creditDisplay;
         if (surface._terrainProvider.ready && defined(surface._terrainProvider.credit)) {
@@ -400,7 +401,7 @@ define([
         var vertexArraysToDestroy = this._vertexArraysToDestroy;
         var length = vertexArraysToDestroy.length;
         for (var j = 0; j < length; ++j) {
-            freeVertexArray(vertexArraysToDestroy[j]);
+            GlobeSurfaceTile._freeVertexArray(vertexArraysToDestroy[j]);
         }
         vertexArraysToDestroy.length = 0;
     };
@@ -425,6 +426,9 @@ define([
             clippingPlanes.update(frameState);
         }
         this._usedDrawCommands = 0;
+
+        this._hasLoadedTilesThisFrame = false;
+        this._hasFillTilesThisFrame = false;
     };
 
     /**
@@ -457,6 +461,12 @@ define([
             });
         }
 
+        // If this frame has a mix of loaded and fill tiles, we need to propagate
+        // loaded heights to the fill tiles.
+        if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) {
+            TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState, this._vertexArraysToDestroy);
+        }
+
         // Add the tile render commands to the command list, sorted by texture count.
         var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;
         for (var textureCountIndex = 0, textureCountLength = tilesToRenderByTextureCount.length; textureCountIndex < textureCountLength; ++textureCountIndex) {
@@ -513,12 +523,33 @@ define([
      * @exception {DeveloperError} <code>loadTile</code> must not be called before the tile provider is ready.
      */
     GlobeSurfaceTileProvider.prototype.loadTile = function(frameState, tile) {
-        GlobeSurfaceTile.processStateMachine(tile, frameState, this._terrainProvider, this._imageryLayers, this._vertexArraysToDestroy);
-        var tileLoadedEvent = this._tileLoadedEvent;
-        tile._loadedCallbacks['tileLoadedEvent'] = function (tile) {
-            tileLoadedEvent.raiseEvent();
-            return true;
-        };
+        // We don't want to load imagery until we're certain that the terrain tiles are actually visible.
+        // So if our bounding volume isn't accurate because it came from another tile, load terrain only
+        // initially. If we load some terrain and suddenly have a more accurate bounding volume and the
+        // tile is _still_ visible, give the tile a chance to load imagery immediately rather than
+        // waiting for next frame.
+
+        var surfaceTile = tile.data;
+        var terrainOnly = true;
+        var terrainStateBefore;
+        if (defined(surfaceTile)) {
+            terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile || tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED;
+            terrainStateBefore = surfaceTile.terrainState;
+        }
+
+        GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly);
+
+        surfaceTile = tile.data;
+        if (terrainOnly && terrainStateBefore !== tile.data.terrainState) {
+            // Terrain state changed. If:
+            // a) The tile is visible, and
+            // b) The bounding volume is accurate (updated as a side effect of computing visibility)
+            // Then we'll load imagery, too.
+            if (this.computeTileVisibility(tile, frameState, this.quadtree.occluders) && surfaceTile.boundingVolumeSourceTile === tile) {
+                terrainOnly = false;
+                GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly);
+            }
+        }
     };
 
     var boundingSphereScratch = new BoundingSphere();
@@ -564,8 +595,15 @@ define([
         }
 
         var surfaceTile = tile.data;
+        var tileBoundingRegion = surfaceTile.tileBoundingRegion;
+
+        if (surfaceTile.boundingVolumeSourceTile === undefined) {
+            // We have no idea where this tile is, so let's just call it partially visible.
+            return Visibility.PARTIAL;
+        }
+
         var cullingVolume = frameState.cullingVolume;
-        var boundingVolume = defaultValue(surfaceTile.orientedBoundingBox, surfaceTile.boundingSphere3D);
+        var boundingVolume = surfaceTile.orientedBoundingBox;
 
         // Check if the tile is outside the limit area in cartographic space
         surfaceTile.clippedByBoundaries = false;
@@ -580,11 +618,11 @@ define([
 
         if (frameState.mode !== SceneMode.SCENE3D) {
             boundingVolume = boundingSphereScratch;
-            BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, surfaceTile.minimumHeight, surfaceTile.maximumHeight, boundingVolume);
+            BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume);
             Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);
 
-            if (frameState.mode === SceneMode.MORPHING) {
-                boundingVolume = BoundingSphere.union(surfaceTile.boundingSphere3D, boundingVolume, boundingVolume);
+            if (frameState.mode === SceneMode.MORPHING && defined(surfaceTile.renderedMesh)) {
+                boundingVolume = BoundingSphere.union(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume, boundingVolume);
             }
         }
 
@@ -619,6 +657,150 @@ define([
         return intersection;
     };
 
+    /**
+     * Determines if the given tile can be refined
+     * @param {QuadtreeTile} tile The tile to check.
+     * @returns {boolean} True if the tile can be refined, false if it cannot.
+     */
+    GlobeSurfaceTileProvider.prototype.canRefine = function(tile) {
+        // Only allow refinement it we know whether or not the children of this tile exist.
+        // For a tileset with `availability`, we'll always be able to refine.
+        // We can ask for availability of _any_ child tile because we only need to confirm
+        // that we get a yes or no answer, it doesn't matter what the answer is.
+        if (defined(tile.data.terrainData)) {
+            return true;
+        }
+        var childAvailable = this.terrainProvider.getTileDataAvailable(tile.x * 2, tile.y * 2, tile.level + 1);
+        return childAvailable !== undefined;
+    };
+
+    var readyImageryScratch = [];
+    var canRenderTraversalStack = [];
+
+    /**
+     * Determines if the given not-fully-loaded tile can be rendered without losing detail that
+     * was present last frame as a result of rendering descendant tiles. This method will only be
+     * called if this tile's descendants were rendered last frame. If the tile is fully loaded,
+     * it is assumed that this method will return true and it will not be called.
+     * @param {QuadtreeTile} tile The tile to check.
+     * @returns {boolean} True if the tile can be rendered without losing detail.
+     */
+    GlobeSurfaceTileProvider.prototype.canRenderWithoutLosingDetail = function(tile, frameState) {
+        var surfaceTile = tile.data;
+
+        var readyImagery = readyImageryScratch;
+        readyImagery.length = this._imageryLayers.length;
+
+        var terrainReady = false;
+        var initialImageryState = false;
+        var imagery;
+
+        if (defined(surfaceTile)) {
+            // We can render even with non-ready terrain as long as all our rendered descendants
+            // are missing terrain geometry too. i.e. if we rendered fills for more detailed tiles
+            // last frame, it's ok to render a fill for this tile this frame.
+            terrainReady = surfaceTile.terrainState === TerrainState.READY;
+
+            // Initially assume all imagery layers are ready, unless imagery hasn't been initialized at all.
+            initialImageryState = true;
+
+            imagery = surfaceTile.imagery;
+        }
+
+        var i;
+        var len;
+
+        for (i = 0, len = readyImagery.length; i < len; ++i) {
+            readyImagery[i] = initialImageryState;
+        }
+
+        if (defined(imagery)) {
+            for (i = 0, len = imagery.length; i < len; ++i) {
+                var tileImagery = imagery[i];
+                var loadingImagery = tileImagery.loadingImagery;
+                var isReady = !defined(loadingImagery) || loadingImagery.state === ImageryState.FAILED || loadingImagery.state === ImageryState.INVALID;
+                var layerIndex = (tileImagery.loadingImagery || tileImagery.readyImagery).imageryLayer._layerIndex;
+
+                // For a layer to be ready, all tiles belonging to that layer must be ready.
+                readyImagery[layerIndex] = isReady && readyImagery[layerIndex];
+            }
+        }
+
+        var lastFrame = this.quadtree._lastSelectionFrameNumber;
+
+        // Traverse the descendants looking for one with terrain or imagery that is not loaded on this tile.
+        var stack = canRenderTraversalStack;
+        stack.length = 0;
+        stack.push(tile.southwestChild, tile.southeastChild, tile.northwestChild, tile.northeastChild);
+
+        while (stack.length > 0) {
+            var descendant = stack.pop();
+            var lastFrameSelectionResult = descendant._lastSelectionResultFrame === lastFrame ? descendant._lastSelectionResult : TileSelectionResult.NONE;
+
+            if (lastFrameSelectionResult === TileSelectionResult.RENDERED) {
+                var descendantSurface = descendant.data;
+
+                if (!defined(descendantSurface)) {
+                    // Descendant has no data, so it can't block rendering.
+                    continue;
+                }
+
+                if (!terrainReady && descendant.data.terrainState === TerrainState.READY) {
+                    // Rendered descendant has real terrain, but we don't. Rendering is blocked.
+                    return false;
+                }
+
+                var descendantImagery = descendant.data.imagery;
+                for (i = 0, len = descendantImagery.length; i < len; ++i) {
+                    var descendantTileImagery = descendantImagery[i];
+                    var descendantLoadingImagery = descendantTileImagery.loadingImagery;
+                    var descendantIsReady = !defined(descendantLoadingImagery) || descendantLoadingImagery.state === ImageryState.FAILED || descendantLoadingImagery.state === ImageryState.INVALID;
+                    var descendantLayerIndex = (descendantTileImagery.loadingImagery || descendantTileImagery.readyImagery).imageryLayer._layerIndex;
+
+                    // If this imagery tile of a descendant is ready but the layer isn't ready in this tile,
+                    // then rendering is blocked.
+                    if (descendantIsReady && !readyImagery[descendantLayerIndex]) {
+                        return false;
+                    }
+                }
+            } else if (lastFrameSelectionResult === TileSelectionResult.REFINED) {
+                stack.push(descendant.southwestChild, descendant.southeastChild, descendant.northwestChild, descendant.northeastChild);
+            }
+        }
+
+        return true;
+    };
+
+    var tileDirectionScratch = new Cartesian3();
+
+    /**
+     * Determines the priority for loading this tile. Lower priority values load sooner.
+     * @param {QuatreeTile} tile The tile.
+     * @param {FrameState} frameState The frame state.
+     * @returns {Number} The load priority value.
+     */
+    GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) {
+        var surfaceTile = tile.data;
+        if (surfaceTile === undefined) {
+            return 0.0;
+        }
+
+        var obb = surfaceTile.orientedBoundingBox;
+        if (obb === undefined) {
+            return 0.0;
+        }
+
+        var cameraPosition = frameState.camera.positionWC;
+        var cameraDirection = frameState.camera.directionWC;
+        var tileDirection = Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch);
+        var magnitude = Cartesian3.magnitude(tileDirection);
+        if (magnitude < CesiumMath.EPSILON5) {
+            return 0.0;
+        }
+        Cartesian3.divideByScalar(tileDirection, magnitude, tileDirection);
+        return (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance;
+    };
+
     var modifiedModelViewScratch = new Matrix4();
     var modifiedModelViewProjectionScratch = new Matrix4();
     var tileRectangleScratch = new Cartesian4();
@@ -633,7 +815,7 @@ define([
      * render commands to the commandList, or use any other method as appropriate.  The tile is not
      * expected to be visible next frame as well, unless this method is called next frame, too.
      *
-     * @param {Object} tile The tile instance.
+     * @param {QuadtreeTile} tile The tile instance.
      * @param {FrameState} frameState The state information of the current rendering frame.
      */
     GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState) {
@@ -654,11 +836,33 @@ define([
 
         tileSet.push(tile);
 
+        var surfaceTile = tile.data;
+        if (!defined(surfaceTile.vertexArray)) {
+            this._hasFillTilesThisFrame = true;
+        } else {
+            this._hasLoadedTilesThisFrame = true;
+        }
+
         var debug = this._debug;
         ++debug.tilesRendered;
         debug.texturesRendered += readyTextureCount;
     };
 
+    var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];
+
+    function computeOccludeePoint(tileProvider, center, rectangle, height, result) {
+        var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid;
+        var ellipsoid = ellipsoidalOccluder.ellipsoid;
+
+        var cornerPositions = cornerPositionsScratch;
+        Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]);
+        Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]);
+        Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]);
+        Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]);
+
+        return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result);
+    }
+
     /**
      * Gets the distance from the camera to the closest point on the tile.  This is used for level-of-detail selection.
      *
@@ -668,11 +872,128 @@ define([
      * @returns {Number} The distance from the camera to the closest point on the tile, in meters.
      */
     GlobeSurfaceTileProvider.prototype.computeDistanceToTile = function(tile, frameState) {
+        // The distance should be:
+        // 1. the actual distance to the tight-fitting bounding volume, or
+        // 2. a distance that is equal to or greater than the actual distance to the tight-fitting bounding volume.
+        //
+        // When we don't know the min/max heights for a tile, but we do know the min/max of an ancestor tile, we can
+        // build a tight-fitting bounding volume horizontally, but not vertically. The min/max heights from the
+        // ancestor will likely form a volume that is much bigger than it needs to be. This means that the volume may
+        // be deemed to be much closer to the camera than it really is, causing us to select tiles that are too detailed.
+        // Loading too-detailed tiles is super expensive, so we don't want to do that. We don't know where the child
+        // tile really lies within the parent range of heights, but we _do_ know the child tile can't be any closer than
+        // the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we compute distance
+        // based that conservative metric, we may end up loading tiles that are not detailed enough, but that's much
+        // better (faster) than loading tiles that are too detailed.
+
+        var heightSource = updateTileBoundingRegion(tile, this.terrainProvider, frameState);
         var surfaceTile = tile.data;
         var tileBoundingRegion = surfaceTile.tileBoundingRegion;
-        return tileBoundingRegion.distanceToCamera(frameState);
+
+        if (heightSource === undefined) {
+            // Can't find any min/max heights anywhere? Ok, let's just say the
+            // tile is really far away so we'll load and render it rather than
+            // refining.
+            return 9999999999.0;
+        } else if (surfaceTile.boundingVolumeSourceTile !== heightSource) {
+            // Heights are from a new source tile, so update the bounding volume.
+            surfaceTile.boundingVolumeSourceTile = heightSource;
+            surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle(
+                tile.rectangle,
+                tileBoundingRegion.minimumHeight,
+                tileBoundingRegion.maximumHeight,
+                tile.tilingScheme.ellipsoid,
+                surfaceTile.orientedBoundingBox);
+
+            surfaceTile.occludeePointInScaledSpace = computeOccludeePoint(this, surfaceTile.orientedBoundingBox.center, tile.rectangle, tileBoundingRegion.maximumHeight, surfaceTile.occludeePointInScaledSpace);
+        }
+
+        var min = tileBoundingRegion.minimumHeight;
+        var max = tileBoundingRegion.maximumHeight;
+
+        if (surfaceTile.boundingVolumeSourceTile !== tile) {
+            var cameraHeight = frameState.camera.positionCartographic.height;
+            var distanceToMin = Math.abs(cameraHeight - min);
+            var distanceToMax = Math.abs(cameraHeight - max);
+            if (distanceToMin > distanceToMax) {
+                tileBoundingRegion.minimumHeight = min;
+                tileBoundingRegion.maximumHeight = min;
+            } else {
+                tileBoundingRegion.minimumHeight = max;
+                tileBoundingRegion.maximumHeight = max;
+            }
+        }
+
+        var result = tileBoundingRegion.distanceToCamera(frameState);
+
+        tileBoundingRegion.minimumHeight = min;
+        tileBoundingRegion.maximumHeight = max;
+
+        return result;
     };
 
+    function updateTileBoundingRegion(tile, terrainProvider, frameState) {
+        var surfaceTile = tile.data;
+        if (surfaceTile === undefined) {
+            surfaceTile = tile.data = new GlobeSurfaceTile();
+        }
+
+        if (surfaceTile.tileBoundingRegion === undefined) {
+            surfaceTile.tileBoundingRegion = new TileBoundingRegion({
+                computeBoundingVolumes : false,
+                rectangle : tile.rectangle,
+                ellipsoid : tile.tilingScheme.ellipsoid,
+                minimumHeight : 0.0,
+                maximumHeight : 0.0
+            });
+        }
+
+        var terrainData = surfaceTile.terrainData;
+        var mesh = surfaceTile.mesh;
+        var tileBoundingRegion = surfaceTile.tileBoundingRegion;
+
+        if (mesh !== undefined && mesh.minimumHeight !== undefined && mesh.maximumHeight !== undefined) {
+            // We have tight-fitting min/max heights from the mesh.
+            tileBoundingRegion.minimumHeight = mesh.minimumHeight;
+            tileBoundingRegion.maximumHeight = mesh.maximumHeight;
+            return tile;
+        }
+
+        if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) {
+            // We have tight-fitting min/max heights from the terrain data.
+            tileBoundingRegion.minimumHeight = terrainData._minimumHeight * frameState.terrainExaggeration;
+            tileBoundingRegion.maximumHeight = terrainData._maximumHeight * frameState.terrainExaggeration;
+            return tile;
+        }
+
+        // No accurate min/max heights available, so we're stuck with min/max heights from an ancestor tile.
+        tileBoundingRegion.minimumHeight = Number.NaN;
+        tileBoundingRegion.maximumHeight = Number.NaN;
+
+        var ancestor = tile.parent;
+        while (ancestor !== undefined) {
+            var ancestorSurfaceTile = ancestor.data;
+            if (ancestorSurfaceTile !== undefined) {
+                var ancestorMesh = ancestorSurfaceTile.mesh;
+                if (ancestorMesh !== undefined && ancestorMesh.minimumHeight !== undefined && ancestorMesh.maximumHeight !== undefined) {
+                    tileBoundingRegion.minimumHeight = ancestorMesh.minimumHeight;
+                    tileBoundingRegion.maximumHeight = ancestorMesh.maximumHeight;
+                    return ancestor;
+                }
+
+                var ancestorTerrainData = ancestorSurfaceTile.terrainData;
+                if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) {
+                    tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight * frameState.terrainExaggeration;
+                    tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight * frameState.terrainExaggeration;
+                    return ancestor;
+                }
+            }
+            ancestor = ancestor.parent;
+        }
+
+        return undefined;
+    }
+
     /**
      * Returns true if this object was destroyed; otherwise, false.
      * <br /><br />
@@ -808,6 +1129,12 @@ define([
             this._quadtree.forEachLoadedTile(function(tile) {
                 if (layer._createTileImagerySkeletons(tile, terrainProvider)) {
                     tile.state = QuadtreeTileLoadState.LOADING;
+
+                    // Tiles that are not currently being rendered need to load the new layer before they're renderable.
+                    // We don't mark the rendered tiles non-renderable, though, because that would make the globe disappear.
+                    if (tile.level !== 0 && (tile._lastSelectionResultFrame !== that.quadtree._lastSelectionFrameNumber || tile._lastSelectionResult !== TileSelectionResult.RENDERED)) {
+                        tile.renderable = false;
+                    }
                 }
             });
 
@@ -873,6 +1200,9 @@ define([
             u_initialColor : function() {
                 return this.properties.initialColor;
             },
+            u_fillHighlightColor : function() {
+                return this.properties.fillHighlightColor;
+            },
             u_zoomedOutOceanSpecularIntensity : function() {
                 return this.properties.zoomedOutOceanSpecularIntensity;
             },
@@ -993,6 +1323,7 @@ define([
             // derived commands that combine another uniform map with this one.
             properties : {
                 initialColor : new Cartesian4(0.0, 0.0, 0.5, 1.0),
+                fillHighlightColor : new Color(0.0, 0.0, 0.0, 0.0),
                 zoomedOutOceanSpecularIntensity : 0.5,
                 oceanNormalMap : undefined,
                 lightingFadeDistance : new Cartesian2(6500000.0, 9000000.0),
@@ -1039,15 +1370,32 @@ define([
     function createWireframeVertexArrayIfNecessary(context, provider, tile) {
         var surfaceTile = tile.data;
 
-        if (defined(surfaceTile.wireframeVertexArray)) {
-            return;
+        var mesh;
+        var vertexArray;
+
+        if (defined(surfaceTile.vertexArray)) {
+            mesh = surfaceTile.mesh;
+            vertexArray = surfaceTile.vertexArray;
+        } else if (defined(surfaceTile.fill) && defined(surfaceTile.fill.vertexArray)) {
+            mesh = surfaceTile.fill.mesh;
+            vertexArray = surfaceTile.fill.vertexArray;
         }
 
-        if (!defined(surfaceTile.terrainData) || !defined(surfaceTile.terrainData._mesh)) {
+        if (!defined(mesh) || !defined(vertexArray)) {
             return;
         }
 
-        surfaceTile.wireframeVertexArray = createWireframeVertexArray(context, surfaceTile.vertexArray, surfaceTile.terrainData._mesh);
+        if (defined(surfaceTile.wireframeVertexArray)) {
+            if (surfaceTile.wireframeVertexArray.mesh === mesh) {
+                return;
+            }
+
+            surfaceTile.wireframeVertexArray.destroy();
+            surfaceTile.wireframeVertexArray = undefined;
+        }
+
+        surfaceTile.wireframeVertexArray = createWireframeVertexArray(context, vertexArray, mesh);
+        surfaceTile.wireframeVertexArray.mesh = mesh;
     }
 
     /**
@@ -1062,8 +1410,10 @@ define([
      * @returns {VertexArray} The vertex array for wireframe rendering.
      */
     function createWireframeVertexArray(context, vertexArray, terrainMesh) {
+        var indices = terrainMesh.indices;
+
         var geometry = {
-            indices : terrainMesh.indices,
+            indices : indices,
             primitiveType : PrimitiveType.TRIANGLES
         };
 
@@ -1074,7 +1424,7 @@ define([
             context : context,
             typedArray : wireframeIndices,
             usage : BufferUsage.STATIC_DRAW,
-            indexDatatype : IndexDatatype.UNSIGNED_SHORT
+            indexDatatype : IndexDatatype.fromSizeInBytes(wireframeIndices.BYTES_PER_ELEMENT)
         });
         return new VertexArray({
             context : context,
@@ -1180,6 +1530,17 @@ define([
 
     function addDrawCommandsForTile(tileProvider, tile, frameState) {
         var surfaceTile = tile.data;
+
+        if (!defined(surfaceTile.vertexArray)) {
+            if (surfaceTile.fill === undefined) {
+                // No fill was created for this tile, probably because this tile is not connected to
+                // any renderable tiles. So create a simple tile in the middle of the tile's possible
+                // height range.
+                surfaceTile.fill = new TerrainFillMesh(tile);
+            }
+            surfaceTile.fill.update(tileProvider, frameState);
+        }
+
         var creditDisplay = frameState.creditDisplay;
 
         var terrainData = surfaceTile.terrainData;
@@ -1194,6 +1555,12 @@ define([
         var maxTextures = ContextLimits.maximumTextureImageUnits;
 
         var waterMaskTexture = surfaceTile.waterMaskTexture;
+        var waterMaskTranslationAndScale = surfaceTile.waterMaskTranslationAndScale;
+        if (!defined(waterMaskTexture) && defined(surfaceTile.fill)) {
+            waterMaskTexture = surfaceTile.fill.waterMaskTexture;
+            waterMaskTranslationAndScale = surfaceTile.fill.waterMaskTranslationAndScale;
+        }
+
         var showReflectiveOcean = tileProvider.hasWaterMask && defined(waterMaskTexture);
         var oceanNormalMap = tileProvider.oceanNormalMap;
         var showOceanWaves = showReflectiveOcean && defined(oceanNormalMap);
@@ -1235,8 +1602,9 @@ define([
             --maxTextures;
         }
 
-        var rtc = surfaceTile.center;
-        var encoding = surfaceTile.pickTerrain.mesh.encoding;
+        var mesh = surfaceTile.renderedMesh;
+        var rtc = mesh.center;
+        var encoding = mesh.encoding;
 
         // Not used in 3D.
         var tileRectangle = tileRectangleScratch;
@@ -1352,13 +1720,14 @@ define([
             ++tileProvider._usedDrawCommands;
 
             if (tile === tileProvider._debug.boundingSphereTile) {
+                var obb = surfaceTile.orientedBoundingBox;
                 // If a debug primitive already exists for this tile, it will not be
                 // re-created, to avoid allocation every frame. If it were possible
                 // to have more than one selected tile, this would have to change.
-                if (defined(surfaceTile.orientedBoundingBox)) {
-                    getDebugOrientedBoundingBox(surfaceTile.orientedBoundingBox, Color.RED).update(frameState);
-                } else if (defined(surfaceTile.boundingSphere3D)) {
-                    getDebugBoundingSphere(surfaceTile.boundingSphere3D, Color.RED).update(frameState);
+                if (defined(obb)) {
+                    getDebugOrientedBoundingBox(obb, Color.RED).update(frameState);
+                } else if (defined(mesh) && defined(mesh.boundingSphere3D)) {
+                    getDebugBoundingSphere(mesh.boundingSphere3D, Color.RED).update(frameState);
                 }
             }
 
@@ -1371,7 +1740,12 @@ define([
             uniformMapProperties.nightFadeDistance.y = tileProvider.nightFadeInDistance;
             uniformMapProperties.zoomedOutOceanSpecularIntensity = tileProvider.zoomedOutOceanSpecularIntensity;
 
-            uniformMapProperties.center3D = surfaceTile.center;
+            var highlightFillTile = !defined(surfaceTile.vertexArray) && defined(tileProvider.fillHighlightColor) && tileProvider.fillHighlightColor.alpha > 0.0;
+            if (highlightFillTile) {
+                Color.clone(tileProvider.fillHighlightColor, uniformMapProperties.fillHighlightColor);
+            }
+
+            uniformMapProperties.center3D = mesh.center;
             Cartesian3.clone(rtc, uniformMapProperties.rtc);
 
             Cartesian4.clone(tileRectangle, uniformMapProperties.tileRectangle);
@@ -1500,7 +1874,7 @@ define([
             // which might get destroyed eventually
             uniformMapProperties.dayTextures.length = numberOfDayTextures;
             uniformMapProperties.waterMask = waterMaskTexture;
-            Cartesian4.clone(surfaceTile.waterMaskTranslationAndScale, uniformMapProperties.waterMaskTranslationAndScale);
+            Cartesian4.clone(waterMaskTranslationAndScale, uniformMapProperties.waterMaskTranslationAndScale);
 
             uniformMapProperties.minMaxHeight.x = encoding.minimumHeight;
             uniformMapProperties.minMaxHeight.y = encoding.maximumHeight;
@@ -1531,13 +1905,14 @@ define([
             surfaceShaderSetOptions.clippingPlanes = clippingPlanes;
             surfaceShaderSetOptions.hasImageryLayerCutout = applyCutout;
             surfaceShaderSetOptions.colorCorrect = colorCorrect;
+            surfaceShaderSetOptions.highlightFillTile = highlightFillTile;
 
             command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(surfaceShaderSetOptions);
             command.castShadows = castShadows;
             command.receiveShadows = receiveShadows;
             command.renderState = renderState;
             command.primitiveType = PrimitiveType.TRIANGLES;
-            command.vertexArray = surfaceTile.vertexArray;
+            command.vertexArray = surfaceTile.vertexArray || surfaceTile.fill.vertexArray;
             command.uniformMap = uniformMap;
             command.pass = Pass.GLOBE;
 
@@ -1553,14 +1928,15 @@ define([
             var orientedBoundingBox = command.orientedBoundingBox;
 
             if (frameState.mode !== SceneMode.SCENE3D) {
-                BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, surfaceTile.minimumHeight, surfaceTile.maximumHeight, boundingVolume);
+                var tileBoundingRegion = surfaceTile.tileBoundingRegion;
+                BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume);
                 Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);
 
                 if (frameState.mode === SceneMode.MORPHING) {
-                    boundingVolume = BoundingSphere.union(surfaceTile.boundingSphere3D, boundingVolume, boundingVolume);
+                    boundingVolume = BoundingSphere.union(mesh.boundingSphere3D, boundingVolume, boundingVolume);
                 }
             } else {
-                command.boundingVolume = BoundingSphere.clone(surfaceTile.boundingSphere3D, boundingVolume);
+                command.boundingVolume = BoundingSphere.clone(mesh.boundingSphere3D, boundingVolume);
                 command.orientedBoundingBox = OrientedBoundingBox.clone(surfaceTile.orientedBoundingBox, orientedBoundingBox);
             }
 
diff --git a/Source/Scene/Imagery.js b/Source/Scene/Imagery.js
index e486a2a804e..75c9c216758 100644
--- a/Source/Scene/Imagery.js
+++ b/Source/Scene/Imagery.js
@@ -84,10 +84,10 @@ define([
         return this.referenceCount;
     };
 
-    Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, priorityFunction) {
-        if (this.state === ImageryState.UNLOADED) {
+    Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, skipLoading) {
+        if (this.state === ImageryState.UNLOADED && !skipLoading) {
             this.state = ImageryState.TRANSITIONING;
-            this.imageryLayer._requestImagery(this, priorityFunction);
+            this.imageryLayer._requestImagery(this);
         }
 
         if (this.state === ImageryState.RECEIVED) {
diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js
index 321a797cead..4618414bd55 100644
--- a/Source/Scene/ImageryLayer.js
+++ b/Source/Scene/ImageryLayer.js
@@ -720,9 +720,8 @@ define([
      * @private
      *
      * @param {Imagery} imagery The imagery to request.
-     * @param {Function} [priorityFunction] The priority function used for sorting the imagery request.
      */
-    ImageryLayer.prototype._requestImagery = function(imagery, priorityFunction) {
+    ImageryLayer.prototype._requestImagery = function(imagery) {
         var imageryProvider = this._imageryProvider;
 
         var that = this;
@@ -765,10 +764,9 @@ define([
 
         function doRequest() {
             var request = new Request({
-                throttle : true,
+                throttle : false,
                 throttleByServer : true,
-                type : RequestType.IMAGERY,
-                priorityFunction : priorityFunction
+                type : RequestType.IMAGERY
             });
             imagery.request = request;
             imagery.state = ImageryState.TRANSITIONING;
@@ -791,6 +789,34 @@ define([
         doRequest();
     };
 
+    ImageryLayer.prototype._createTextureWebGL = function(context, imagery) {
+        var sampler = new Sampler({
+            minificationFilter : this.minificationFilter,
+            magnificationFilter : this.magnificationFilter
+        });
+
+        var image = imagery.image;
+
+        if (defined(image.internalFormat)) {
+            return new Texture({
+                context : context,
+                pixelFormat : image.internalFormat,
+                width : image.width,
+                height : image.height,
+                source : {
+                    arrayBufferView : image.bufferView
+                },
+                sampler : sampler
+            });
+        }
+        return new Texture({
+            context : context,
+            source : image,
+            pixelFormat : this._imageryProvider.hasAlphaChannel ? PixelFormat.RGBA : PixelFormat.RGB,
+            sampler : sampler
+        });
+    };
+
     /**
      * Create a WebGL texture for a given {@link Imagery} instance.
      *
@@ -830,32 +856,8 @@ define([
         }
         //>>includeEnd('debug');
 
-        var sampler = new Sampler({
-            minificationFilter : this.minificationFilter,
-            magnificationFilter : this.magnificationFilter
-        });
-
         // Imagery does not need to be discarded, so upload it to WebGL.
-        var texture;
-        if (defined(image.internalFormat)) {
-            texture = new Texture({
-                context : context,
-                pixelFormat : image.internalFormat,
-                width : image.width,
-                height : image.height,
-                source : {
-                    arrayBufferView : image.bufferView
-                },
-                sampler : sampler
-            });
-        } else {
-            texture = new Texture({
-                context : context,
-                source : image,
-                pixelFormat : imageryProvider.hasAlphaChannel ? PixelFormat.RGBA : PixelFormat.RGB,
-                sampler : sampler
-            });
-        }
+        var texture = this._createTextureWebGL(context, imagery);
 
         if (imageryProvider.tilingScheme.projection instanceof WebMercatorProjection) {
             imagery.textureWebMercator = texture;
@@ -870,16 +872,16 @@ define([
         return minificationFilter + ':' + magnificationFilter + ':' + maximumAnisotropy;
     }
 
-    function finalizeReprojectTexture(imageryLayer, context, imagery, texture) {
-        var minificationFilter = imageryLayer.minificationFilter;
-        var magnificationFilter = imageryLayer.magnificationFilter;
+    ImageryLayer.prototype._finalizeReprojectTexture = function(context, texture) {
+        var minificationFilter = this.minificationFilter;
+        var magnificationFilter = this.magnificationFilter;
         var usesLinearTextureFilter = minificationFilter === TextureMinificationFilter.LINEAR && magnificationFilter === TextureMagnificationFilter.LINEAR;
         // Use mipmaps if this texture has power-of-two dimensions.
         // In addition, mipmaps are only generated if the texture filters are both LINEAR.
         if (usesLinearTextureFilter && !PixelFormat.isCompressedFormat(texture.pixelFormat) && CesiumMath.isPowerOfTwo(texture.width) && CesiumMath.isPowerOfTwo(texture.height)) {
             minificationFilter = TextureMinificationFilter.LINEAR_MIPMAP_LINEAR;
             var maximumSupportedAnisotropy = ContextLimits.maximumTextureFilterAnisotropy;
-            var maximumAnisotropy = Math.min(maximumSupportedAnisotropy, defaultValue(imageryLayer._maximumAnisotropy, maximumSupportedAnisotropy));
+            var maximumAnisotropy = Math.min(maximumSupportedAnisotropy, defaultValue(this._maximumAnisotropy, maximumSupportedAnisotropy));
             var mipmapSamplerKey = getSamplerKey(minificationFilter, magnificationFilter, maximumAnisotropy);
             var mipmapSamplers = context.cache.imageryLayerMipmapSamplers;
             if (!defined(mipmapSamplers)) {
@@ -916,9 +918,7 @@ define([
             }
             texture.sampler = nonMipmapSampler;
         }
-
-        imagery.state = ImageryState.READY;
-    }
+    };
 
     /**
      * Enqueues a command re-projecting a texture to a {@link GeographicProjection} on the next update, if necessary, and generate
@@ -956,7 +956,8 @@ define([
                     },
                     postExecute : function(outputTexture) {
                         imagery.texture = outputTexture;
-                        finalizeReprojectTexture(that, context, imagery, outputTexture);
+                        that._finalizeReprojectTexture(context, outputTexture);
+                        imagery.state = ImageryState.READY;
                         imagery.releaseReference();
                     }
                 });
@@ -965,7 +966,8 @@ define([
             if (needGeographicProjection) {
                 imagery.texture = texture;
             }
-            finalizeReprojectTexture(this, context, imagery, texture);
+            this._finalizeReprojectTexture(context, texture);
+            imagery.state = ImageryState.READY;
         }
     };
 
diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js
index 39ec5703e3d..d923303b331 100644
--- a/Source/Scene/QuadtreePrimitive.js
+++ b/Source/Scene/QuadtreePrimitive.js
@@ -8,7 +8,9 @@ define([
         '../Core/Event',
         '../Core/getTimestamp',
         '../Core/Math',
+        '../Core/Matrix4',
         '../Core/OrthographicFrustum',
+        '../Core/OrthographicOffCenterFrustum',
         '../Core/Ray',
         '../Core/Rectangle',
         '../Core/Visibility',
@@ -16,7 +18,8 @@ define([
         './QuadtreeTile',
         './QuadtreeTileLoadState',
         './SceneMode',
-        './TileReplacementQueue'
+        './TileReplacementQueue',
+        './TileSelectionResult'
     ], function(
         Cartesian3,
         Cartographic,
@@ -27,7 +30,9 @@ define([
         Event,
         getTimestamp,
         CesiumMath,
+        Matrix4,
         OrthographicFrustum,
+        OrthographicOffCenterFrustum,
         Ray,
         Rectangle,
         Visibility,
@@ -35,7 +40,8 @@ define([
         QuadtreeTile,
         QuadtreeTileLoadState,
         SceneMode,
-        TileReplacementQueue) {
+        TileReplacementQueue,
+        TileSelectionResult) {
     'use strict';
 
     /**
@@ -76,12 +82,14 @@ define([
             enableDebugOutput : false,
 
             maxDepth : 0,
+            maxDepthVisited : 0,
             tilesVisited : 0,
             tilesCulled : 0,
             tilesRendered : 0,
             tilesWaitingForChildren : 0,
 
             lastMaxDepth : -1,
+            lastMaxDepthVisited : -1,
             lastTilesVisited : -1,
             lastTilesCulled : -1,
             lastTilesRendered : -1,
@@ -109,6 +117,13 @@ define([
         this._lastTileIndex = 0;
         this._updateHeightsTimeSlice = 2.0;
 
+        // If a culled tile contains _cameraPositionCartographic or _cameraReferenceFrameOriginCartographic, it will be marked
+        // TileSelectionResult.CULLED_BUT_NEEDED and added to the list of tiles to update heights,
+        // even though it is not rendered.
+        // These are updated each frame in `selectTilesForRendering`.
+        this._cameraPositionCartographic = undefined;
+        this._cameraReferenceFrameOriginCartographic = undefined;
+
         /**
          * Gets or sets the maximum screen-space error, in pixels, that is allowed.
          * A higher maximum error will render fewer tiles and improve performance, while a lower
@@ -128,12 +143,46 @@ define([
          */
         this.tileCacheSize = defaultValue(options.tileCacheSize, 100);
 
+        /**
+         * Gets or sets the number of loading descendant tiles that is considered "too many".
+         * If a tile has too many loading descendants, that tile will be loaded and rendered before any of
+         * its descendants are loaded and rendered. This means more feedback for the user that something
+         * is happening at the cost of a longer overall load time. Setting this to 0 will cause each
+         * tile level to be loaded successively, significantly increasing load time. Setting it to a large
+         * number (e.g. 1000) will minimize the number of tiles that are loaded but tend to make
+         * detail appear all at once after a long wait.
+         * @type {Number}
+         * @default 20
+         */
+        this.loadingDescendantLimit = 20;
+
+        /**
+         * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded.
+         * Setting this to true optimizes the zoom-out experience and provides more detail in
+         * newly-exposed areas when panning. The down side is that it requires loading more tiles.
+         * @type {Boolean}
+         * @default true
+         */
+        this.preloadAncestors = true;
+
+        /**
+         * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded.
+         * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even
+         * if they are culled. Setting this to true may provide a better panning experience at the
+         * cost of loading more tiles.
+         * @type {Boolean}
+         * @default false
+         */
+        this.preloadSiblings = false;
+
         this._occluders = new QuadtreeOccluders({
             ellipsoid : ellipsoid
         });
 
         this._tileLoadProgressEvent = new Event();
         this._lastTileLoadQueueLength = 0;
+
+        this._lastSelectionFrameNumber = undefined;
     }
 
     defineProperties(QuadtreePrimitive.prototype, {
@@ -158,6 +207,12 @@ define([
             get : function() {
                 return this._tileLoadProgressEvent;
             }
+        },
+
+        occluders : {
+            get : function() {
+                return this._occluders;
+            }
         }
     });
 
@@ -280,6 +335,7 @@ define([
     function clearTileLoadQueue(primitive) {
         var debug = primitive._debug;
         debug.maxDepth = 0;
+        debug.maxDepthVisited = 0;
         debug.tilesVisited = 0;
         debug.tilesCulled = 0;
         debug.tilesRendered = 0;
@@ -308,11 +364,12 @@ define([
         // Gets commands for any texture re-projections
         this._tileProvider.initialize(frameState);
 
+        clearTileLoadQueue(this);
+
         if (this._debug.suspendLodUpdate) {
             return;
         }
 
-        clearTileLoadQueue(this);
         this._tileReplacementQueue.markStartOfRenderFrame();
     };
 
@@ -351,20 +408,27 @@ define([
         }
 
         var debug = primitive._debug;
-        if (debug.enableDebugOutput  && !debug.suspendLodUpdate) {
+        if (debug.enableDebugOutput && !debug.suspendLodUpdate) {
+            debug.maxDepth = primitive._tilesToRender.reduce(function(max, tile) {
+                return Math.max(max, tile.level);
+            }, -1);
+            debug.tilesRendered = primitive._tilesToRender.length;
+
             if (debug.tilesVisited !== debug.lastTilesVisited ||
                 debug.tilesRendered !== debug.lastTilesRendered ||
                 debug.tilesCulled !== debug.lastTilesCulled ||
                 debug.maxDepth !== debug.lastMaxDepth ||
-                debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren) {
+                debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren ||
+                debug.maxDepthVisited !== debug.lastMaxDepthVisited) {
 
-                console.log('Visited ' + debug.tilesVisited + ', Rendered: ' + debug.tilesRendered + ', Culled: ' + debug.tilesCulled + ', Max Depth: ' + debug.maxDepth + ', Waiting for children: ' + debug.tilesWaitingForChildren);
+                console.log('Visited ' + debug.tilesVisited + ', Rendered: ' + debug.tilesRendered + ', Culled: ' + debug.tilesCulled + ', Max Depth Rendered: ' + debug.maxDepth + ', Max Depth Visited: ' + debug.maxDepthVisited + ', Waiting for children: ' + debug.tilesWaitingForChildren);
 
                 debug.lastTilesVisited = debug.tilesVisited;
                 debug.lastTilesRendered = debug.tilesRendered;
                 debug.lastTilesCulled = debug.tilesCulled;
                 debug.lastMaxDepth = debug.maxDepth;
                 debug.lastTilesWaitingForChildren = debug.tilesWaitingForChildren;
+                debug.lastMaxDepthVisited = debug.maxDepthVisited;
             }
         }
     }
@@ -439,6 +503,9 @@ define([
         return (alon * alon + alat * alat) - (blon * blon + blat * blat);
     }
 
+    var cameraOriginScratch = new Cartesian3();
+    var rootTraversalDetails = [];
+
     function selectTilesForRendering(primitive, frameState) {
         var debug = primitive._debug;
         if (debug.suspendLodUpdate) {
@@ -450,11 +517,21 @@ define([
         tilesToRender.length = 0;
 
         // We can't render anything before the level zero tiles exist.
+        var i;
         var tileProvider = primitive._tileProvider;
         if (!defined(primitive._levelZeroTiles)) {
             if (tileProvider.ready) {
                 var tilingScheme = tileProvider.tilingScheme;
                 primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+                var numberOfRootTiles = primitive._levelZeroTiles.length;
+                if (rootTraversalDetails.length < numberOfRootTiles) {
+                    rootTraversalDetails = new Array(numberOfRootTiles);
+                    for (i = 0; i < numberOfRootTiles; ++i) {
+                        if (rootTraversalDetails[i] === undefined) {
+                            rootTraversalDetails[i] = new TraversalDetails();
+                        }
+                    }
+                }
             } else {
                 // Nothing to do until the provider is ready.
                 return;
@@ -477,7 +554,6 @@ define([
         var customDataRemoved = primitive._removeHeightCallbacks;
         var frameNumber = frameState.frameNumber;
 
-        var i;
         var len;
         if (customDataAdded.length > 0 || customDataRemoved.length > 0) {
             for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
@@ -489,49 +565,118 @@ define([
             customDataRemoved.length = 0;
         }
 
-        // Our goal with load ordering is to first load all of the tiles we need to
-        // render the current scene at full detail.  Loading any other tiles is just
-        // a form of prefetching, and we need not do it at all (other concerns aside).  This
-        // simple and obvious statement gets more complicated when we realize that, because
-        // we don't have bounding volumes for the entire terrain tile pyramid, we don't
-        // precisely know which tiles we need to render the scene at full detail, until we do
-        // some loading.
-        //
-        // So our load priority is (from high to low):
-        // 1. Tiles that we _would_ render, except that they're not sufficiently loaded yet.
-        //    Ideally this would only include tiles that we've already determined to be visible,
-        //    but since we don't have reliable visibility information until a tile is loaded,
-        //    and because we (currently) must have all children in a quad renderable before we
-        //    can refine, this pretty much means tiles we'd like to refine to, regardless of
-        //    visibility. (high)
-        // 2. Tiles that we're rendering. (medium)
-        // 3. All other tiles. (low)
-        //
-        // Within each priority group, tiles should be loaded in approximate near-to-far order,
-        // but currently they're just loaded in our traversal order which makes no guarantees
-        // about depth ordering.
+        var camera = frameState.camera;
+
+        primitive._cameraPositionCartographic = camera.positionCartographic;
+        var cameraFrameOrigin = Matrix4.getTranslation(camera.transform, cameraOriginScratch);
+        primitive._cameraReferenceFrameOriginCartographic = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(cameraFrameOrigin, primitive._cameraReferenceFrameOriginCartographic);
 
         // Traverse in depth-first, near-to-far order.
         for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
             tile = levelZeroTiles[i];
             primitive._tileReplacementQueue.markTileRendered(tile);
             if (!tile.renderable) {
-                if (tile.needsLoading) {
-                    primitive._tileLoadQueueHigh.push(tile);
-                }
+                queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
                 ++debug.tilesWaitingForChildren;
-            } else if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) {
-                visitTile(primitive, frameState, tile);
             } else {
-                if (tile.needsLoading) {
-                    primitive._tileLoadQueueLow.push(tile);
-                }
-                ++debug.tilesCulled;
+                visitIfVisible(primitive, tile, tileProvider, frameState, occluders, false, rootTraversalDetails[i]);
             }
         }
+
+        primitive._lastSelectionFrameNumber = frameNumber;
+    }
+
+    function queueTileLoad(primitive, queue, tile, frameState) {
+        if (!tile.needsLoading) {
+            return;
+        }
+
+        if (primitive.tileProvider.computeTileLoadPriority !== undefined) {
+            tile._loadPriority = primitive.tileProvider.computeTileLoadPriority(tile, frameState);
+        }
+        queue.push(tile);
+    }
+
+    /**
+     * Tracks details of traversing a tile while selecting tiles for rendering.
+     * @alias TraversalDetails
+     * @constructor
+     * @private
+     */
+    function TraversalDetails() {
+        /**
+         * True if all selected (i.e. not culled or refined) tiles in this tile's subtree
+         * are renderable. If the subtree is renderable, we'll render it; no drama.
+         */
+        this.allAreRenderable = true;
+
+        /**
+         * True if any tiles in this tile's subtree were rendered last frame. If any
+         * were, we must render the subtree rather than this tile, because rendering
+         * this tile would cause detail to vanish that was visible last frame, and
+         * that's no good.
+         */
+        this.anyWereRenderedLastFrame = false;
+
+        /**
+         * Counts the number of selected tiles in this tile's subtree that are
+         * not yet ready to be rendered because they need more loading. Note that
+         * this value will _not_ necessarily be zero when
+         * {@link TraversalDetails#allAreRenderable} is true, for subtle reasons.
+         * When {@link TraversalDetails#allAreRenderable} and
+         * {@link TraversalDetails#anyWereRenderedLastFrame} are both false, we
+         * will render this tile instead of any tiles in its subtree and
+         * the `allAreRenderable` value for this tile will reflect only whether _this_
+         * tile is renderable. The `notYetRenderableCount` value, however, will still
+         * reflect the total number of tiles that we are waiting on, including the
+         * ones that we're not rendering. `notYetRenderableCount` is only reset
+         * when a subtree is removed from the render queue because the
+         * `notYetRenderableCount` exceeds the
+         * {@link QuadtreePrimitive#loadingDescendantLimit}.
+         */
+        this.notYetRenderableCount = 0;
+    }
+
+    function TraversalQuadDetails() {
+        this.southwest = new TraversalDetails();
+        this.southeast = new TraversalDetails();
+        this.northwest = new TraversalDetails();
+        this.northeast = new TraversalDetails();
+    }
+
+    TraversalQuadDetails.prototype.combine = function(result) {
+        var southwest = this.southwest;
+        var southeast = this.southeast;
+        var northwest = this.northwest;
+        var northeast = this.northeast;
+
+        result.allAreRenderable = southwest.allAreRenderable && southeast.allAreRenderable && northwest.allAreRenderable && northeast.allAreRenderable;
+        result.anyWereRenderedLastFrame = southwest.anyWereRenderedLastFrame || southeast.anyWereRenderedLastFrame || northwest.anyWereRenderedLastFrame || northeast.anyWereRenderedLastFrame;
+        result.notYetRenderableCount = southwest.notYetRenderableCount + southeast.notYetRenderableCount + northwest.notYetRenderableCount + northeast.notYetRenderableCount;
+    };
+
+    var traversalQuadsByLevel = new Array(30); // level 30 tiles are ~2cm wide at the equator, should be good enough.
+    for (var i = 0; i < traversalQuadsByLevel.length; ++i) {
+        traversalQuadsByLevel[i] = new TraversalQuadDetails();
     }
 
-    function visitTile(primitive, frameState, tile) {
+    /**
+     * Visits a tile for possible rendering. When we call this function with a tile:
+     *
+     *    * the tile has been determined to be visible (possibly based on a bounding volume that is not very tight-fitting)
+     *    * its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, see comments below)
+     *    * the tile may or may not be renderable
+     *
+     * @private
+     *
+     * @param {Primitive} primitive The QuadtreePrimitive.
+     * @param {FrameState} frameState The frame state.
+     * @param {QuadtreeTile} tile The tile to visit
+     * @param {Boolean} ancestorMeetsSse True if a tile higher in the tile tree already met the SSE and we're refining further only
+     *                  to maintain detail while that higher tile loads.
+     * @param {TraversalDetails} traveralDetails On return, populated with details of how the traversal of this tile went.
+     */
+    function visitTile(primitive, frameState, tile, ancestorMeetsSse, traversalDetails) {
         var debug = primitive._debug;
 
         ++debug.tilesVisited;
@@ -539,160 +684,304 @@ define([
         primitive._tileReplacementQueue.markTileRendered(tile);
         tile._updateCustomData(frameState.frameNumber);
 
-        if (tile.level > debug.maxDepth) {
-            debug.maxDepth = tile.level;
+        if (tile.level > debug.maxDepthVisited) {
+            debug.maxDepthVisited = tile.level;
         }
 
-        if (screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError) {
-            // This tile meets SSE requirements, so render it.
-            if (tile.needsLoading) {
-                // Rendered tile meeting SSE loads with medium priority.
-                primitive._tileLoadQueueMedium.push(tile);
-            }
-            addTileToRenderList(primitive, tile);
-            return;
-        }
+        var meetsSse = screenSpaceError(primitive, frameState, tile) < primitive.maximumScreenSpaceError;
 
         var southwestChild = tile.southwestChild;
         var southeastChild = tile.southeastChild;
         var northwestChild = tile.northwestChild;
         var northeastChild = tile.northeastChild;
-        var allAreRenderable = southwestChild.renderable && southeastChild.renderable &&
-                               northwestChild.renderable && northeastChild.renderable;
-        var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent &&
-                              northwestChild.upsampledFromParent && northeastChild.upsampledFromParent;
 
-        if (allAreRenderable) {
-            if (allAreUpsampled) {
-                // No point in rendering the children because they're all upsampled.  Render this tile instead.
+        var lastFrame = primitive._lastSelectionFrameNumber;
+        var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE;
+
+        var tileProvider = primitive.tileProvider;
+
+        if (meetsSse || ancestorMeetsSse) {
+            // This tile (or an ancestor) is the one we want to render this frame, but we'll do different things depending
+            // on the state of this tile and on what we did _last_ frame.
+
+            // We can render it if _any_ of the following are true:
+            // 1. We rendered it (or kicked it) last frame.
+            // 2. This tile was culled last frame, or it wasn't even visited because an ancestor was culled.
+            // 3. The tile is completely done loading.
+            // 4. a) Terrain is ready, and
+            //    b) All necessary imagery is ready. Necessary imagery is imagery that was rendered with this tile
+            //       or any descendants last frame. Such imagery is required because rendering this tile without
+            //       it would cause detail to disappear.
+            //
+            // Determining condition 4 is more expensive, so we check the others first.
+            //
+            // Note that even if we decide to render a tile here, it may later get "kicked" in favor of an ancestor.
+
+            var oneRenderedLastFrame = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.RENDERED;
+            var twoCulledOrNotVisited = TileSelectionResult.originalResult(lastFrameSelectionResult) === TileSelectionResult.CULLED || lastFrameSelectionResult === TileSelectionResult.NONE;
+            var threeCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE;
+
+            var renderable = oneRenderedLastFrame || twoCulledOrNotVisited || threeCompletelyLoaded;
+
+            if (!renderable) {
+                // Check the more expensive condition 4 above. This requires details of the thing
+                // we're rendering (e.g. the globe surface), so delegate it to the tile provider.
+                if (defined(tileProvider.canRenderWithoutLosingDetail)) {
+                    renderable = tileProvider.canRenderWithoutLosingDetail(tile);
+                }
+            }
+
+            if (renderable) {
+                // Only load this tile if it (not just an ancestor) meets the SSE.
+                if (meetsSse) {
+                    queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState);
+                }
                 addTileToRenderList(primitive, tile);
 
-                // Load the children even though we're (currently) not going to render them.
-                // A tile that is "upsampled only" right now might change its tune once it does more loading.
-                // A tile that is upsampled now and forever should also be done loading, so no harm done.
-                queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild);
+                traversalDetails.allAreRenderable = tile.renderable;
+                traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED;
+                traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
 
-                if (tile.needsLoading) {
-                    // Rendered tile that's not waiting on children loads with medium priority.
-                    primitive._tileLoadQueueMedium.push(tile);
-                }
-            } else {
-                // SSE is not good enough and children are loaded, so refine.
-                // No need to add the children to the load queue because they'll be added (if necessary) when they're visited.
-                visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState);
+                tile._lastSelectionResultFrame = frameState.frameNumber;
+                tile._lastSelectionResult = TileSelectionResult.RENDERED;
 
-                if (tile.needsLoading) {
-                    // Tile is not rendered, so load it with low priority.
-                    primitive._tileLoadQueueLow.push(tile);
+                if (!traversalDetails.anyWereRenderedLastFrame) {
+                    // Tile is newly-rendered this frame, so update its heights.
+                    primitive._tileToUpdateHeights.push(tile);
                 }
+
+                return;
             }
-        } else {
-            // We'd like to refine but can't because not all of our children are renderable.  Load the refinement blockers with high priority and
-            // render this tile in the meantime.
-            queueChildLoadNearToFar(primitive, frameState.camera.positionCartographic, southwestChild, southeastChild, northwestChild, northeastChild);
-            addTileToRenderList(primitive, tile);
-
-            if (tile.needsLoading) {
-                // We will refine this tile when it's possible, so load this tile only with low priority.
-                primitive._tileLoadQueueLow.push(tile);
+
+            // Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear
+            // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered
+            // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last
+            // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level
+            // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better
+            // than required. So fall through to continue traversal...
+            ancestorMeetsSse = true;
+
+            // Load this blocker tile with high priority, but only if this tile (not just an ancestor) meets the SSE.
+            if (meetsSse) {
+                queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
             }
         }
-    }
 
-    function queueChildLoadNearToFar(primitive, cameraPosition, southwest, southeast, northwest, northeast) {
-        if (cameraPosition.longitude < southwest.east) {
-            if (cameraPosition.latitude < southwest.north) {
-                // Camera in southwest quadrant
-                queueChildTileLoad(primitive, southwest);
-                queueChildTileLoad(primitive, southeast);
-                queueChildTileLoad(primitive, northwest);
-                queueChildTileLoad(primitive, northeast);
-            } else {
-                // Camera in northwest quadrant
-                queueChildTileLoad(primitive, northwest);
-                queueChildTileLoad(primitive, southwest);
-                queueChildTileLoad(primitive, northeast);
-                queueChildTileLoad(primitive, southeast);
+        if (tileProvider.canRefine(tile)) {
+            var allAreUpsampled = southwestChild.upsampledFromParent && southeastChild.upsampledFromParent &&
+                                  northwestChild.upsampledFromParent && northeastChild.upsampledFromParent;
+
+            if (allAreUpsampled) {
+                // No point in rendering the children because they're all upsampled.  Render this tile instead.
+                addTileToRenderList(primitive, tile);
+
+                // Rendered tile that's not waiting on children loads with medium priority.
+                queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState);
+
+                // Make sure we don't unload the children and forget they're upsampled.
+                primitive._tileReplacementQueue.markTileRendered(southwestChild);
+                primitive._tileReplacementQueue.markTileRendered(southeastChild);
+                primitive._tileReplacementQueue.markTileRendered(northwestChild);
+                primitive._tileReplacementQueue.markTileRendered(northeastChild);
+
+                traversalDetails.allAreRenderable = tile.renderable;
+                traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED;
+                traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
+
+                tile._lastSelectionResultFrame = frameState.frameNumber;
+                tile._lastSelectionResult = TileSelectionResult.RENDERED;
+
+                if (!traversalDetails.anyWereRenderedLastFrame) {
+                    // Tile is newly-rendered this frame, so update its heights.
+                    primitive._tileToUpdateHeights.push(tile);
+                }
+
+                return;
             }
-        } else if (cameraPosition.latitude < southwest.north) {
-            // Camera southeast quadrant
-            queueChildTileLoad(primitive, southeast);
-            queueChildTileLoad(primitive, southwest);
-            queueChildTileLoad(primitive, northeast);
-            queueChildTileLoad(primitive, northwest);
-        } else {
-            // Camera in northeast quadrant
-            queueChildTileLoad(primitive, northeast);
-            queueChildTileLoad(primitive, northwest);
-            queueChildTileLoad(primitive, southeast);
-            queueChildTileLoad(primitive, southwest);
-        }
-    }
 
-    function queueChildTileLoad(primitive, childTile) {
-        primitive._tileReplacementQueue.markTileRendered(childTile);
-        if (childTile.needsLoading) {
-            if (childTile.renderable) {
-                primitive._tileLoadQueueLow.push(childTile);
-            } else {
-                // A tile blocking refine loads with high priority
-                primitive._tileLoadQueueHigh.push(childTile);
+            // SSE is not good enough, so refine.
+            tile._lastSelectionResultFrame = frameState.frameNumber;
+            tile._lastSelectionResult = TileSelectionResult.REFINED;
+
+            var firstRenderedDescendantIndex = primitive._tilesToRender.length;
+            var loadIndexLow = primitive._tileLoadQueueLow.length;
+            var loadIndexMedium = primitive._tileLoadQueueMedium.length;
+            var loadIndexHigh = primitive._tileLoadQueueHigh.length;
+            var tilesToUpdateHeightsIndex = primitive._tileToUpdateHeights.length;
+
+            // No need to add the children to the load queue because they'll be added (if necessary) when they're visited.
+            visitVisibleChildrenNearToFar(primitive, southwestChild, southeastChild, northwestChild, northeastChild, frameState, ancestorMeetsSse, traversalDetails);
+
+            // If no descendant tiles were added to the render list by the function above, it means they were all
+            // culled even though this tile was deemed visible. That's pretty common.
+
+            if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) {
+                // At least one descendant tile was added to the render list.
+                // The traversalDetails tell us what happened while visiting the children.
+
+                var allAreRenderable = traversalDetails.allAreRenderable;
+                var anyWereRenderedLastFrame = traversalDetails.anyWereRenderedLastFrame;
+                var notYetRenderableCount = traversalDetails.notYetRenderableCount;
+                var queuedForLoad = false;
+
+                if (!allAreRenderable && !anyWereRenderedLastFrame) {
+                    // Some of our descendants aren't ready to render yet, and none were rendered last frame,
+                    // so kick them all out of the render list and render this tile instead. Continue to load them though!
+
+                    // Mark the rendered descendants and their ancestors - up to this tile - as kicked.
+                    var renderList = primitive._tilesToRender;
+                    for (var i = firstRenderedDescendantIndex; i < renderList.length; ++i) {
+                        var workTile = renderList[i];
+                        while (workTile !== undefined && workTile._lastSelectionResult !== TileSelectionResult.KICKED && workTile !== tile) {
+                            workTile._lastSelectionResult = TileSelectionResult.kick(workTile._lastSelectionResult);
+                            workTile = workTile.parent;
+                        }
+                    }
+
+                    // Remove all descendants from the render list and add this tile.
+                    primitive._tilesToRender.length = firstRenderedDescendantIndex;
+                    primitive._tileToUpdateHeights.length = tilesToUpdateHeightsIndex;
+                    addTileToRenderList(primitive, tile);
+
+                    tile._lastSelectionResult = TileSelectionResult.RENDERED;
+
+                    // If we're waiting on heaps of descendants, the above will take too long. So in that case,
+                    // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting
+                    // on this tile. Keep doing this until we actually manage to render this tile.
+                    var wasRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED;
+                    if (!wasRenderedLastFrame && notYetRenderableCount > primitive.loadingDescendantLimit) {
+                        // Remove all descendants from the load queues.
+                        primitive._tileLoadQueueLow.length = loadIndexLow;
+                        primitive._tileLoadQueueMedium.length = loadIndexMedium;
+                        primitive._tileLoadQueueHigh.length = loadIndexHigh;
+                        queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState);
+                        traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
+                        queuedForLoad = true;
+                    }
+
+                    traversalDetails.allAreRenderable = tile.renderable;
+                    traversalDetails.anyWereRenderedLastFrame = wasRenderedLastFrame;
+
+                    if (!wasRenderedLastFrame) {
+                        // Tile is newly-rendered this frame, so update its heights.
+                        primitive._tileToUpdateHeights.push(tile);
+                    }
+
+                    ++debug.tilesWaitingForChildren;
+                }
+
+                if (primitive.preloadAncestors && !queuedForLoad) {
+                    queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState);
+                }
             }
+
+            return;
         }
+
+        tile._lastSelectionResultFrame = frameState.frameNumber;
+        tile._lastSelectionResult = TileSelectionResult.RENDERED;
+
+        // We'd like to refine but can't because we have no availability data for this tile's children,
+        // so we have no idea if refinining would involve a load or an upsample. We'll have to finish
+        // loading this tile first in order to find that out, so load this refinement blocker with
+        // high priority.
+        addTileToRenderList(primitive, tile);
+        queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
+
+        traversalDetails.allAreRenderable = tile.renderable;
+        traversalDetails.anyWereRenderedLastFrame = lastFrameSelectionResult === TileSelectionResult.RENDERED;
+        traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
     }
 
-    function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState) {
+    function visitVisibleChildrenNearToFar(primitive, southwest, southeast, northwest, northeast, frameState, ancestorMeetsSse, traversalDetails) {
         var cameraPosition = frameState.camera.positionCartographic;
         var tileProvider = primitive._tileProvider;
         var occluders = primitive._occluders;
 
+        var quadDetails = traversalQuadsByLevel[southwest.level];
+        var southwestDetails = quadDetails.southwest;
+        var southeastDetails = quadDetails.southeast;
+        var northwestDetails = quadDetails.northwest;
+        var northeastDetails = quadDetails.northeast;
+
         if (cameraPosition.longitude < southwest.rectangle.east) {
             if (cameraPosition.latitude < southwest.rectangle.north) {
                 // Camera in southwest quadrant
-                visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
+                visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails);
+                visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails);
+                visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails);
+                visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails);
             } else {
                 // Camera in northwest quadrant
-                visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
-                visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
+                visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails);
+                visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails);
+                visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails);
+                visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails);
             }
         } else if (cameraPosition.latitude < southwest.rectangle.north) {
             // Camera southeast quadrant
-            visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
+            visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails);
+            visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails);
+            visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails);
+            visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails);
         } else {
             // Camera in northeast quadrant
-            visitIfVisible(primitive, northeast, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, northwest, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, southeast, tileProvider, frameState, occluders);
-            visitIfVisible(primitive, southwest, tileProvider, frameState, occluders);
+            visitIfVisible(primitive, northeast, tileProvider, frameState, occluders, ancestorMeetsSse, northeastDetails);
+            visitIfVisible(primitive, northwest, tileProvider, frameState, occluders, ancestorMeetsSse, northwestDetails);
+            visitIfVisible(primitive, southeast, tileProvider, frameState, occluders, ancestorMeetsSse, southeastDetails);
+            visitIfVisible(primitive, southwest, tileProvider, frameState, occluders, ancestorMeetsSse, southwestDetails);
         }
+
+        quadDetails.combine(traversalDetails);
     }
 
-    function visitIfVisible(primitive, tile, tileProvider, frameState, occluders) {
+    function containsNeededPosition(primitive, tile) {
+        var rectangle = tile.rectangle;
+        return (defined(primitive._cameraPositionCartographic) && Rectangle.contains(rectangle, primitive._cameraPositionCartographic)) ||
+               (defined(primitive._cameraReferenceFrameOriginCartographic) && Rectangle.contains(rectangle, primitive._cameraReferenceFrameOriginCartographic));
+    }
+
+    function visitIfVisible(primitive, tile, tileProvider, frameState, occluders, ancestorMeetsSse, traversalDetails) {
         if (tileProvider.computeTileVisibility(tile, frameState, occluders) !== Visibility.NONE) {
-            visitTile(primitive, frameState, tile);
-        } else {
-            ++primitive._debug.tilesCulled;
-            primitive._tileReplacementQueue.markTileRendered(tile);
+            return visitTile(primitive, frameState, tile, ancestorMeetsSse, traversalDetails);
+        }
+
+        ++primitive._debug.tilesCulled;
+        primitive._tileReplacementQueue.markTileRendered(tile);
 
-            // We've decided this tile is not visible, but if it's not fully loaded yet, we've made
-            // this determination based on possibly-incorrect information.  We need to load this
-            // culled tile with low priority just in case it turns out to be visible after all.
-            if (tile.needsLoading) {
-                primitive._tileLoadQueueLow.push(tile);
+        traversalDetails.allAreRenderable = true;
+        traversalDetails.anyWereRenderedLastFrame = false;
+        traversalDetails.notYetRenderableCount = 0;
+
+        if (containsNeededPosition(primitive, tile)) {
+            // Load the tile(s) that contains the camera's position and
+            // the origin of its reference frame with medium priority.
+            // But we only need to load until the terrain is available, no need to load imagery.
+            if (!defined(tile.data) || !defined(tile.data.vertexArray)) {
+                queueTileLoad(primitive, primitive._tileLoadQueueMedium, tile, frameState);
+            }
+
+            var lastFrame = primitive._lastSelectionFrameNumber;
+            var lastFrameSelectionResult = tile._lastSelectionResultFrame === lastFrame ? tile._lastSelectionResult : TileSelectionResult.NONE;
+            if (lastFrameSelectionResult !== TileSelectionResult.CULLED_BUT_NEEDED && lastFrameSelectionResult !== TileSelectionResult.RENDERED) {
+                primitive._tileToUpdateHeights.push(tile);
             }
+
+            tile._lastSelectionResult = TileSelectionResult.CULLED_BUT_NEEDED;
+        } else if (primitive.preloadSiblings || tile.level === 0) {
+            // Load culled level zero tiles with low priority.
+            // For all other levels, only load culled tiles if preloadSiblings is enabled.
+            queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState);
+            tile._lastSelectionResult = TileSelectionResult.CULLED;
+        } else {
+            tile._lastSelectionResult = TileSelectionResult.CULLED;
         }
+
+        tile._lastSelectionResultFrame = frameState.frameNumber;
     }
 
     function screenSpaceError(primitive, frameState, tile) {
-        if (frameState.mode === SceneMode.SCENE2D || frameState.camera.frustum instanceof OrthographicFrustum) {
+        if (frameState.mode === SceneMode.SCENE2D || frameState.camera.frustum instanceof OrthographicFrustum || frameState.camera.frustum instanceof OrthographicOffCenterFrustum) {
             return screenSpaceError2D(primitive, frameState, tile);
         }
 
@@ -735,7 +1024,6 @@ define([
 
     function addTileToRenderList(primitive, tile) {
         primitive._tilesToRender.push(tile);
-        ++primitive._debug.tilesRendered;
     }
 
     function processTileLoadQueue(primitive, frameState) {
@@ -754,17 +1042,28 @@ define([
         var endTime = getTimestamp() + primitive._loadQueueTimeSlice;
         var tileProvider = primitive._tileProvider;
 
-        processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh);
-        processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium);
-        processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow);
+        var didSomeLoading = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueHigh, false);
+        didSomeLoading = processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueMedium, didSomeLoading);
+        processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, tileLoadQueueLow, didSomeLoading);
+    }
+
+    function sortByLoadPriority(a, b) {
+        return a._loadPriority - b._loadPriority;
     }
 
-    function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue) {
-        for (var i = 0, len = loadQueue.length; i < len && getTimestamp() < endTime; ++i) {
+    function processSinglePriorityLoadQueue(primitive, frameState, tileProvider, endTime, loadQueue, didSomeLoading) {
+        if (tileProvider.computeTileLoadPriority !== undefined) {
+            loadQueue.sort(sortByLoadPriority);
+        }
+
+        for (var i = 0, len = loadQueue.length; i < len && (getTimestamp() < endTime || !didSomeLoading); ++i) {
             var tile = loadQueue[i];
             primitive._tileReplacementQueue.markTileRendered(tile);
             tileProvider.loadTile(frameState, tile);
+            didSomeLoading = true;
         }
+
+        return didSomeLoading;
     }
 
     var scratchRay = new Ray();
@@ -773,6 +1072,10 @@ define([
     var scratchArray = [];
 
     function updateHeights(primitive, frameState) {
+        if (!primitive.tileProvider.ready) {
+            return;
+        }
+
         var tryNextFrame = scratchArray;
         tryNextFrame.length = 0;
         var tilesToUpdateHeights = primitive._tileToUpdateHeights;
@@ -784,13 +1087,20 @@ define([
 
         var mode = frameState.mode;
         var projection = frameState.mapProjection;
-        var ellipsoid = projection.ellipsoid;
+        var ellipsoid = primitive.tileProvider.tilingScheme.ellipsoid;
         var i;
 
         while (tilesToUpdateHeights.length > 0) {
             var tile = tilesToUpdateHeights[0];
-            if (tile.state !== QuadtreeTileLoadState.DONE) {
-                tryNextFrame.push(tile);
+            if (!defined(tile.data) || !defined(tile.data.mesh)) {
+                // Tile isn't loaded enough yet, so try again next frame if this tile is still
+                // being rendered.
+                var selectionResult = tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber
+                    ? tile._lastSelectionResult
+                    : TileSelectionResult.NONE;
+                if (selectionResult === TileSelectionResult.RENDERED || selectionResult === TileSelectionResult.CULLED_BUT_NEEDED) {
+                    tryNextFrame.push(tile);
+                }
                 tilesToUpdateHeights.shift();
                 primitive._lastTileIndex = 0;
                 continue;
@@ -886,16 +1196,10 @@ define([
     function createRenderCommandsForSelectedTiles(primitive, frameState) {
         var tileProvider = primitive._tileProvider;
         var tilesToRender = primitive._tilesToRender;
-        var tilesToUpdateHeights = primitive._tileToUpdateHeights;
 
         for (var i = 0, len = tilesToRender.length; i < len; ++i) {
             var tile = tilesToRender[i];
             tileProvider.showTileThisFrame(tile, frameState);
-
-            if (tile._frameRendered !== frameState.frameNumber - 1) {
-                tilesToUpdateHeights.push(tile);
-            }
-            tile._frameRendered = frameState.frameNumber;
         }
     }
 
diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js
index d3fe85700bd..1d2bb69018b 100644
--- a/Source/Scene/QuadtreeTile.js
+++ b/Source/Scene/QuadtreeTile.js
@@ -3,13 +3,15 @@ define([
         '../Core/defineProperties',
         '../Core/DeveloperError',
         '../Core/Rectangle',
-        './QuadtreeTileLoadState'
+        './QuadtreeTileLoadState',
+        './TileSelectionResult'
     ], function(
         defined,
         defineProperties,
         DeveloperError,
         Rectangle,
-        QuadtreeTileLoadState) {
+        QuadtreeTileLoadState,
+        TileSelectionResult) {
     'use strict';
 
     /**
@@ -57,20 +59,21 @@ define([
         this._northwestChild = undefined;
         this._northeastChild = undefined;
 
-        // QuadtreeTileReplacementQueue gets/sets these private properties.
-        this._replacementPrevious = undefined;
-        this._replacementNext = undefined;
+        // TileReplacementQueue gets/sets these private properties.
+        this.replacementPrevious = undefined;
+        this.replacementNext = undefined;
 
         // The distance from the camera to this tile, updated when the tile is selected
         // for rendering.  We can get rid of this if we have a better way to sort by
         // distance - for example, by using the natural ordering of a quadtree.
         // QuadtreePrimitive gets/sets this private property.
         this._distance = 0.0;
-        this._priorityFunction = undefined;
+        this._loadPriority = 0.0;
 
         this._customData = [];
         this._frameUpdated = undefined;
-        this._frameRendered = undefined;
+        this._lastSelectionResult = TileSelectionResult.NONE;
+        this._lastSelectionResultFrame = undefined;
         this._loadedCallbacks = {};
 
         /**
@@ -88,7 +91,7 @@ define([
         this.renderable = false;
 
         /**
-         * Gets or set a value indicating whether or not the tile was entire upsampled from its
+         * Gets or set a value indicating whether or not the tile was entirely upsampled from its
          * parent tile.  If all four children of a parent tile were upsampled from the parent,
          * we will render the parent instead of the children even if the LOD indicates that
          * the children would be preferable.
@@ -394,6 +397,107 @@ define([
         }
     });
 
+    QuadtreeTile.prototype.findLevelZeroTile = function(levelZeroTiles, x, y) {
+        var xTiles = this.tilingScheme.getNumberOfXTilesAtLevel(0);
+        if (x < 0) {
+            x += xTiles;
+        } else if (x >= xTiles) {
+            x -= xTiles;
+        }
+
+        if (y < 0 || y >= this.tilingScheme.getNumberOfYTilesAtLevel(0)) {
+            return undefined;
+        }
+
+        return levelZeroTiles.filter(function(tile) {
+            return tile.x === x && tile.y === y;
+        })[0];
+    };
+
+    QuadtreeTile.prototype.findTileToWest = function(levelZeroTiles) {
+        var parent = this.parent;
+        if (parent === undefined) {
+            return this.findLevelZeroTile(levelZeroTiles, this.x - 1, this.y);
+        }
+
+        if (parent.southeastChild === this) {
+            return parent.southwestChild;
+        } else if (parent.northeastChild === this) {
+            return parent.northwestChild;
+        }
+
+        var westOfParent = parent.findTileToWest(levelZeroTiles);
+        if (westOfParent === undefined) {
+            return undefined;
+        } else if (parent.southwestChild === this) {
+            return westOfParent.southeastChild;
+        }
+        return westOfParent.northeastChild;
+    };
+
+    QuadtreeTile.prototype.findTileToEast = function(levelZeroTiles) {
+        var parent = this.parent;
+        if (parent === undefined) {
+            return this.findLevelZeroTile(levelZeroTiles, this.x + 1, this.y);
+        }
+
+        if (parent.southwestChild === this) {
+            return parent.southeastChild;
+        } else if (parent.northwestChild === this) {
+            return parent.northeastChild;
+        }
+
+        var eastOfParent = parent.findTileToEast(levelZeroTiles);
+        if (eastOfParent === undefined) {
+            return undefined;
+        } else if (parent.southeastChild === this) {
+            return eastOfParent.southwestChild;
+        }
+        return eastOfParent.northwestChild;
+    };
+
+    QuadtreeTile.prototype.findTileToSouth = function(levelZeroTiles) {
+        var parent = this.parent;
+        if (parent === undefined) {
+            return this.findLevelZeroTile(levelZeroTiles, this.x, this.y + 1);
+        }
+
+        if (parent.northwestChild === this) {
+            return parent.southwestChild;
+        } else if (parent.northeastChild === this) {
+            return parent.southeastChild;
+        }
+
+        var southOfParent = parent.findTileToSouth(levelZeroTiles);
+        if (southOfParent === undefined) {
+            return undefined;
+        } else if (parent.southwestChild === this) {
+            return southOfParent.northwestChild;
+        }
+        return southOfParent.northeastChild;
+    };
+
+    QuadtreeTile.prototype.findTileToNorth = function(levelZeroTiles) {
+        var parent = this.parent;
+        if (parent === undefined) {
+            return this.findLevelZeroTile(levelZeroTiles, this.x, this.y - 1);
+        }
+
+        if (parent.southwestChild === this) {
+            return parent.northwestChild;
+        } else if (parent.southeastChild === this) {
+            return parent.northeastChild;
+        }
+
+        var northOfParent = parent.findTileToNorth(levelZeroTiles);
+        if (northOfParent === undefined) {
+            return undefined;
+        } else if (parent.northwestChild === this) {
+            return northOfParent.southwestChild;
+        }
+        return northOfParent.southeastChild;
+    };
+
     /**
      * Frees the resources associated with this tile and returns it to the <code>START</code>
      * {@link QuadtreeTileLoadState}.  If the {@link QuadtreeTile#data} property is defined and it
diff --git a/Source/Scene/ShadowMap.js b/Source/Scene/ShadowMap.js
index d8fa87a3a23..851a9260d3d 100644
--- a/Source/Scene/ShadowMap.js
+++ b/Source/Scene/ShadowMap.js
@@ -1541,7 +1541,7 @@ define([
 
         var hasTerrainNormal = false;
         if (isTerrain) {
-            hasTerrainNormal = command.owner.data.pickTerrain.mesh.encoding.hasVertexNormals;
+            hasTerrainNormal = command.owner.data.renderedMesh.encoding.hasVertexNormals;
         }
 
         if (command.receiveShadows && lightShadowMapsEnabled) {
diff --git a/Source/Scene/TerrainFillMesh.js b/Source/Scene/TerrainFillMesh.js
new file mode 100644
index 00000000000..e047b256899
--- /dev/null
+++ b/Source/Scene/TerrainFillMesh.js
@@ -0,0 +1,1223 @@
+define([
+        '../Core/AttributeCompression',
+        '../Core/binarySearch',
+        '../Core/BoundingSphere',
+        '../Core/Cartesian2',
+        '../Core/Cartesian3',
+        '../Core/Cartesian4',
+        '../Core/Cartographic',
+        '../Core/defined',
+        '../Core/HeightmapTerrainData',
+        '../Core/Math',
+        '../Core/DeveloperError',
+        '../Core/OrientedBoundingBox',
+        '../Core/Queue',
+        '../Core/Rectangle',
+        '../Core/TileEdge',
+        '../Core/TerrainEncoding',
+        '../Core/TerrainMesh',
+        '../Core/WebMercatorProjection',
+        './GlobeSurfaceTile',
+        './TileSelectionResult'
+    ], function(
+        AttributeCompression,
+        binarySearch,
+        BoundingSphere,
+        Cartesian2,
+        Cartesian3,
+        Cartesian4,
+        Cartographic,
+        defined,
+        HeightmapTerrainData,
+        CesiumMath,
+        DeveloperError,
+        OrientedBoundingBox,
+        Queue,
+        Rectangle,
+        TileEdge,
+        TerrainEncoding,
+        TerrainMesh,
+        WebMercatorProjection,
+        GlobeSurfaceTile,
+        TileSelectionResult) {
+    'use strict';
+
+    function TerrainFillMesh(tile) {
+        this.tile = tile;
+        this.frameLastUpdated = undefined;
+        this.westMeshes = []; // north to south (CCW)
+        this.westTiles = [];
+        this.southMeshes = []; // west to east (CCW)
+        this.southTiles = [];
+        this.eastMeshes = []; // south to north (CCW)
+        this.eastTiles = [];
+        this.northMeshes = []; // east to west (CCW)
+        this.northTiles = [];
+        this.southwestMesh = undefined;
+        this.southwestTile = undefined;
+        this.southeastMesh = undefined;
+        this.southeastTile = undefined;
+        this.northwestMesh = undefined;
+        this.northwestTile = undefined;
+        this.northeastMesh = undefined;
+        this.northeastTile = undefined;
+        this.changedThisFrame = true;
+        this.visitedFrame = undefined;
+        this.enqueuedFrame = undefined;
+        this.mesh = undefined;
+        this.vertexArray = undefined;
+        this.waterMaskTexture = undefined;
+        this.waterMaskTranslationAndScale = new Cartesian4();
+    }
+
+    TerrainFillMesh.prototype.update = function(tileProvider, frameState, vertexArraysToDestroy) {
+        if (this.changedThisFrame) {
+            createFillMesh(tileProvider, frameState, this.tile, vertexArraysToDestroy);
+            this.changedThisFrame = false;
+        }
+    };
+
+    TerrainFillMesh.prototype.destroy = function(vertexArraysToDestroy) {
+        if (defined(this.vertexArray)) {
+            if (defined(vertexArraysToDestroy)) {
+                vertexArraysToDestroy.push(this.vertexArray);
+            } else {
+                GlobeSurfaceTile._freeVertexArray(this.vertexArray, vertexArraysToDestroy);
+            }
+            this.vertexArray = undefined;
+        }
+
+        if (defined(this.waterMaskTexture)) {
+            --this.waterMaskTexture.referenceCount;
+            if (this.waterMaskTexture.referenceCount === 0) {
+                this.waterMaskTexture.destroy();
+            }
+            this.waterMaskTexture = undefined;
+        }
+
+        return undefined;
+    };
+
+    var traversalQueueScratch = new Queue();
+
+    TerrainFillMesh.updateFillTiles = function(tileProvider, renderedTiles, frameState, vertexArraysToDestroy) {
+        // We want our fill tiles to look natural, which means they should align perfectly with
+        // adjacent loaded tiles, and their edges that are not adjacent to loaded tiles should have
+        // sensible heights (e.g. the average of the heights of loaded edges). Some fill tiles may
+        // be adjacent only to other fill tiles, and in that case heights should be assigned fanning
+        // outward from the loaded tiles so that there are no sudden changes in height.
+
+        // We do this with a breadth-first traversal of the rendered tiles, starting with the loaded
+        // ones. Graph nodes are tiles and graph edges connect to other rendered tiles that are spatially adjacent
+        // to those tiles. As we visit each node, we propagate tile edges to adjacent tiles. If there's no data
+        // for a tile edge,  we create an edge with an average height and then propagate it. If an edge is partially defined
+        // (e.g. an edge is adjacent to multiple more-detailed tiles and only some of them are loaded), we
+        // fill in the rest of the edge with the same height.
+        var quadtree = tileProvider._quadtree;
+        var levelZeroTiles = quadtree._levelZeroTiles;
+        var lastSelectionFrameNumber = quadtree._lastSelectionFrameNumber;
+
+        var traversalQueue = traversalQueueScratch;
+        traversalQueue.clear();
+
+        // Add the tiles with real geometry to the traversal queue.
+        for (var i = 0; i < renderedTiles.length; ++i) {
+            var renderedTile = renderedTiles[i];
+            if (defined(renderedTile.data.vertexArray)) {
+                traversalQueue.enqueue(renderedTiles[i]);
+            }
+        }
+
+        var tile = traversalQueue.dequeue();
+
+        while (tile !== undefined) {
+            var tileToWest = tile.findTileToWest(levelZeroTiles);
+            var tileToSouth = tile.findTileToSouth(levelZeroTiles);
+            var tileToEast = tile.findTileToEast(levelZeroTiles);
+            var tileToNorth = tile.findTileToNorth(levelZeroTiles);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToWest, lastSelectionFrameNumber, TileEdge.EAST, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToSouth, lastSelectionFrameNumber, TileEdge.NORTH, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToEast, lastSelectionFrameNumber, TileEdge.WEST, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToNorth, lastSelectionFrameNumber, TileEdge.SOUTH, false, traversalQueue, vertexArraysToDestroy);
+
+            var tileToNorthwest = tileToWest.findTileToNorth(levelZeroTiles);
+            var tileToSouthwest = tileToWest.findTileToSouth(levelZeroTiles);
+            var tileToNortheast = tileToEast.findTileToNorth(levelZeroTiles);
+            var tileToSoutheast = tileToEast.findTileToSouth(levelZeroTiles);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToNorthwest, lastSelectionFrameNumber, TileEdge.SOUTHEAST, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToNortheast, lastSelectionFrameNumber, TileEdge.SOUTHWEST, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToSouthwest, lastSelectionFrameNumber, TileEdge.NORTHEAST, false, traversalQueue, vertexArraysToDestroy);
+            visitRenderedTiles(tileProvider, frameState, tile, tileToSoutheast, lastSelectionFrameNumber, TileEdge.NORTHWEST, false, traversalQueue, vertexArraysToDestroy);
+
+            tile = traversalQueue.dequeue();
+        }
+    };
+
+    function visitRenderedTiles(tileProvider, frameState, sourceTile, startTile, currentFrameNumber, tileEdge, downOnly, traversalQueue, vertexArraysToDestroy) {
+        if (startTile === undefined) {
+            // There are no tiles North or South of the poles.
+            return;
+        }
+
+        var tile = startTile;
+        while (tile && (tile._lastSelectionResultFrame !== currentFrameNumber || TileSelectionResult.wasKicked(tile._lastSelectionResult) || TileSelectionResult.originalResult(tile._lastSelectionResult) === TileSelectionResult.CULLED)) {
+            // This tile wasn't visited or it was visited and then kicked, so walk up to find the closest ancestor that was rendered.
+            // We also walk up if the tile was culled, because if siblings were kicked an ancestor may have been rendered.
+            if (downOnly) {
+                return;
+            }
+
+            var parent = tile.parent;
+            if (tileEdge >= TileEdge.NORTHWEST && parent !== undefined) {
+                // When we're looking for a corner, verify that the parent tile is still relevant.
+                // That is, the parent and child must share the corner in question.
+                switch (tileEdge) {
+                    case TileEdge.NORTHWEST:
+                        tile = tile === parent.northwestChild ? parent : undefined;
+                        break;
+                    case TileEdge.NORTHEAST:
+                        tile = tile === parent.northeastChild ? parent : undefined;
+                        break;
+                    case TileEdge.SOUTHWEST:
+                        tile = tile === parent.southwestChild ? parent : undefined;
+                        break;
+                    case TileEdge.SOUTHEAST:
+                        tile = tile === parent.southeastChild ? parent : undefined;
+                        break;
+                }
+            } else {
+                tile = parent;
+            }
+        }
+
+        if (tile === undefined) {
+            return;
+        }
+
+        if (tile._lastSelectionResult === TileSelectionResult.RENDERED) {
+            if (defined(tile.data.vertexArray)) {
+                // No further processing necessary for renderable tiles.
+                return;
+            }
+            visitTile(tileProvider, frameState, sourceTile, tile, tileEdge, currentFrameNumber, traversalQueue, vertexArraysToDestroy);
+            return;
+        }
+
+        if (TileSelectionResult.originalResult(startTile._lastSelectionResult) === TileSelectionResult.CULLED) {
+            return;
+        }
+
+        // This tile was refined, so find rendered children, if any.
+        // Visit the tiles in counter-clockwise order.
+        switch (tileEdge) {
+            case TileEdge.WEST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.EAST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.SOUTH:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.NORTH:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.NORTHWEST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.NORTHEAST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.northeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.SOUTHWEST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southwestChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            case TileEdge.SOUTHEAST:
+                visitRenderedTiles(tileProvider, frameState, sourceTile, startTile.southeastChild, currentFrameNumber, tileEdge, true, traversalQueue, vertexArraysToDestroy);
+                break;
+            default:
+                throw new DeveloperError('Invalid edge');
+        }
+    }
+
+    function visitTile(tileProvider, frameState, sourceTile, destinationTile, tileEdge, frameNumber, traversalQueue, vertexArraysToDestroy) {
+        var destinationSurfaceTile = destinationTile.data;
+
+        if (destinationSurfaceTile.fill === undefined) {
+            destinationSurfaceTile.fill = new TerrainFillMesh(destinationTile);
+        } else if (destinationSurfaceTile.fill.visitedFrame === frameNumber) {
+            // Don't propagate edges to tiles that have already been visited this frame.
+            return;
+        }
+
+        if (destinationSurfaceTile.fill.enqueuedFrame !== frameNumber) {
+            // First time visiting this tile this frame, add it to the traversal queue.
+            destinationSurfaceTile.fill.enqueuedFrame = frameNumber;
+            destinationSurfaceTile.fill.changedThisFrame = false;
+            traversalQueue.enqueue(destinationTile);
+        }
+
+        propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge, vertexArraysToDestroy);
+    }
+
+    function propagateEdge(tileProvider, frameState, sourceTile, destinationTile, tileEdge, vertexArraysToDestroy) {
+        var destinationFill = destinationTile.data.fill;
+
+        var sourceMesh;
+        var sourceFill = sourceTile.data.fill;
+        if (defined(sourceFill)) {
+            sourceFill.visitedFrame = frameState.frameNumber;
+
+            // Source is a fill, create/update it if necessary.
+            if (sourceFill.changedThisFrame) {
+                createFillMesh(tileProvider, frameState, sourceTile, vertexArraysToDestroy);
+                sourceFill.changedThisFrame = false;
+            }
+            sourceMesh = sourceTile.data.fill.mesh;
+        } else {
+            sourceMesh = sourceTile.data.mesh;
+        }
+
+        var edgeMeshes;
+        var edgeTiles;
+
+        switch (tileEdge) {
+            case TileEdge.WEST:
+                edgeMeshes = destinationFill.westMeshes;
+                edgeTiles = destinationFill.westTiles;
+                break;
+            case TileEdge.SOUTH:
+                edgeMeshes = destinationFill.southMeshes;
+                edgeTiles = destinationFill.southTiles;
+                break;
+            case TileEdge.EAST:
+                edgeMeshes = destinationFill.eastMeshes;
+                edgeTiles = destinationFill.eastTiles;
+                break;
+            case TileEdge.NORTH:
+                edgeMeshes = destinationFill.northMeshes;
+                edgeTiles = destinationFill.northTiles;
+                break;
+            // Corners are simpler.
+            case TileEdge.NORTHWEST:
+                destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northwestMesh !== sourceMesh;
+                destinationFill.northwestMesh = sourceMesh;
+                destinationFill.northwestTile = sourceTile;
+                return;
+            case TileEdge.NORTHEAST:
+                destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.northeastMesh !== sourceMesh;
+                destinationFill.northeastMesh = sourceMesh;
+                destinationFill.northeastTile = sourceTile;
+                return;
+            case TileEdge.SOUTHWEST:
+                destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southwestMesh !== sourceMesh;
+                destinationFill.southwestMesh = sourceMesh;
+                destinationFill.southwestTile = sourceTile;
+                return;
+            case TileEdge.SOUTHEAST:
+                destinationFill.changedThisFrame = destinationFill.changedThisFrame || destinationFill.southeastMesh !== sourceMesh;
+                destinationFill.southeastMesh = sourceMesh;
+                destinationFill.southeastTile = sourceTile;
+                return;
+        }
+
+        if (sourceTile.level <= destinationTile.level) {
+            // Source edge completely spans the destination edge.
+            destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[0] !== sourceMesh || edgeMeshes.length !== 1;
+            edgeMeshes[0] = sourceMesh;
+            edgeTiles[0] = sourceTile;
+            edgeMeshes.length = 1;
+            edgeTiles.length = 1;
+            return;
+        }
+
+        // Source edge is a subset of the destination edge.
+        // Figure out the range of meshes we're replacing.
+        var startIndex, endIndex, existingTile, existingRectangle;
+        var sourceRectangle = sourceTile.rectangle;
+
+        var epsilon;
+        var destinationRectangle = destinationTile.rectangle;
+
+        switch (tileEdge) {
+            case TileEdge.WEST:
+                epsilon = (destinationRectangle.north - destinationRectangle.south) * CesiumMath.EPSILON5;
+
+                for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) {
+                    existingTile = edgeTiles[startIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsGreaterThanRight(sourceRectangle.north, existingRectangle.south, epsilon)) {
+                        break;
+                    }
+                }
+                for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) {
+                    existingTile = edgeTiles[endIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsGreaterThanOrEqualToRight(sourceRectangle.south, existingRectangle.north, epsilon)) {
+                        break;
+                    }
+                }
+                break;
+            case TileEdge.SOUTH:
+                epsilon = (destinationRectangle.east - destinationRectangle.west) * CesiumMath.EPSILON5;
+
+                for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) {
+                    existingTile = edgeTiles[startIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsLessThanRight(sourceRectangle.west, existingRectangle.east, epsilon)) {
+                        break;
+                    }
+                }
+                for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) {
+                    existingTile = edgeTiles[endIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsLessThanOrEqualToRight(sourceRectangle.east, existingRectangle.west, epsilon)) {
+                        break;
+                    }
+                }
+                break;
+            case TileEdge.EAST:
+                epsilon = (destinationRectangle.north - destinationRectangle.south) * CesiumMath.EPSILON5;
+
+                for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) {
+                    existingTile = edgeTiles[startIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsLessThanRight(sourceRectangle.south, existingRectangle.north, epsilon)) {
+                        break;
+                    }
+                }
+                for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) {
+                    existingTile = edgeTiles[endIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsLessThanOrEqualToRight(sourceRectangle.north, existingRectangle.south, epsilon)) {
+                        break;
+                    }
+                }
+                break;
+            case TileEdge.NORTH:
+                epsilon = (destinationRectangle.east - destinationRectangle.west) * CesiumMath.EPSILON5;
+
+                for (startIndex = 0; startIndex < edgeTiles.length; ++startIndex) {
+                    existingTile = edgeTiles[startIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsGreaterThanRight(sourceRectangle.east, existingRectangle.west, epsilon)) {
+                        break;
+                    }
+                }
+                for (endIndex = startIndex; endIndex < edgeTiles.length; ++endIndex) {
+                    existingTile = edgeTiles[endIndex];
+                    existingRectangle = existingTile.rectangle;
+                    if (CesiumMath.leftIsGreaterThanOrEqualToRight(sourceRectangle.west, existingRectangle.east, epsilon)) {
+                        break;
+                    }
+                }
+                break;
+        }
+
+        if (endIndex - startIndex === 1) {
+            destinationFill.changedThisFrame = destinationFill.changedThisFrame || edgeMeshes[startIndex] !== sourceMesh;
+            edgeMeshes[startIndex] = sourceMesh;
+            edgeTiles[startIndex] = sourceTile;
+        } else {
+            destinationFill.changedThisFrame = true;
+            edgeMeshes.splice(startIndex, endIndex - startIndex, sourceMesh);
+            edgeTiles.splice(startIndex, endIndex - startIndex, sourceTile);
+        }
+    }
+
+    var cartographicScratch = new Cartographic();
+    var centerCartographicScratch = new Cartographic();
+    var cartesianScratch = new Cartesian3();
+    var normalScratch = new Cartesian3();
+    var octEncodedNormalScratch = new Cartesian2();
+    var uvScratch2 = new Cartesian2();
+    var uvScratch = new Cartesian2();
+
+    function HeightAndNormal() {
+        this.height = 0.0;
+        this.encodedNormal = new Cartesian2();
+    }
+
+    function fillMissingCorner(fill, ellipsoid, u, v, corner, adjacentCorner1, adjacentCorner2, oppositeCorner, vertex) {
+        if (defined(corner)) {
+            return corner;
+        }
+
+        var height;
+
+        if (defined(adjacentCorner1) && defined(adjacentCorner2)) {
+            height = (adjacentCorner1.height + adjacentCorner2.height) * 0.5;
+        } else if (defined(adjacentCorner1)) {
+            height = adjacentCorner1.height;
+        } else if (defined(adjacentCorner2)) {
+            height = adjacentCorner2.height;
+        } else if (defined(oppositeCorner)) {
+            height = oppositeCorner.height;
+        } else {
+            var surfaceTile = fill.tile.data;
+            var tileBoundingRegion = surfaceTile.tileBoundingRegion;
+            var minimumHeight = 0.0;
+            var maximumHeight = 0.0;
+            if (defined(tileBoundingRegion)) {
+                minimumHeight = tileBoundingRegion.minimumHeight;
+                maximumHeight = tileBoundingRegion.maximumHeight;
+            }
+            height = (minimumHeight + maximumHeight) * 0.5;
+        }
+
+        getVertexWithHeightAtCorner(fill, ellipsoid, u, v, height, vertex);
+        return vertex;
+    }
+
+    var heightRangeScratch = {
+        minimumHeight: 0.0,
+        maximumHeight: 0.0
+    };
+
+    var swVertexScratch = new HeightAndNormal();
+    var seVertexScratch = new HeightAndNormal();
+    var nwVertexScratch = new HeightAndNormal();
+    var neVertexScratch = new HeightAndNormal();
+    var heightmapBuffer = typeof Uint8Array !== 'undefined' ? new Uint8Array(9 * 9) : undefined;
+
+    function createFillMesh(tileProvider, frameState, tile, vertexArraysToDestroy) {
+        GlobeSurfaceTile.initialize(tile, tileProvider.terrainProvider, tileProvider._imageryLayers);
+
+        var surfaceTile = tile.data;
+        var fill = surfaceTile.fill;
+        var rectangle = tile.rectangle;
+
+        var ellipsoid = tile.tilingScheme.ellipsoid;
+
+        var nwCorner = getCorner(fill, ellipsoid, 0.0, 1.0, fill.northwestTile, fill.northwestMesh, fill.northTiles, fill.northMeshes, fill.westTiles, fill.westMeshes, nwVertexScratch);
+        var swCorner = getCorner(fill, ellipsoid, 0.0, 0.0, fill.southwestTile, fill.southwestMesh, fill.westTiles, fill.westMeshes, fill.southTiles, fill.southMeshes, swVertexScratch);
+        var seCorner = getCorner(fill, ellipsoid, 1.0, 0.0, fill.southeastTile, fill.southeastMesh, fill.southTiles, fill.southMeshes, fill.eastTiles, fill.eastMeshes, seVertexScratch);
+        var neCorner = getCorner(fill, ellipsoid, 1.0, 1.0, fill.northeastTile, fill.northeastMesh, fill.eastTiles, fill.eastMeshes, fill.northTiles, fill.northMeshes, neVertexScratch);
+
+        nwCorner = fillMissingCorner(fill, ellipsoid, 0.0, 1.0, nwCorner, swCorner, neCorner, seCorner, nwVertexScratch);
+        swCorner = fillMissingCorner(fill, ellipsoid, 0.0, 0.0, swCorner, nwCorner, seCorner, neCorner, swVertexScratch);
+        seCorner = fillMissingCorner(fill, ellipsoid, 1.0, 1.0, seCorner, swCorner, neCorner, nwCorner, seVertexScratch);
+        neCorner = fillMissingCorner(fill, ellipsoid, 1.0, 1.0, neCorner, seCorner, nwCorner, swCorner, neVertexScratch);
+
+        var southwestHeight = swCorner.height;
+        var southeastHeight = seCorner.height;
+        var northwestHeight = nwCorner.height;
+        var northeastHeight = neCorner.height;
+
+        var minimumHeight = Math.min(southwestHeight, southeastHeight, northwestHeight, northeastHeight);
+        var maximumHeight = Math.max(southwestHeight, southeastHeight, northwestHeight, northeastHeight);
+
+        var middleHeight = (minimumHeight + maximumHeight) * 0.5;
+
+        var i;
+        var len;
+
+        // For low-detail tiles, our usual fill tile approach will create tiles that
+        // look really blocky because they don't have enough vertices to account for the
+        // Earth's curvature. But the height range will also typically be well within
+        // the allowed geometric error for those levels. So fill such tiles with a
+        // constant-height heightmap.
+        var geometricError = tileProvider.getLevelMaximumGeometricError(tile.level);
+        var minCutThroughRadius = ellipsoid.maximumRadius - geometricError;
+        var maxTileWidth = Math.acos(minCutThroughRadius / ellipsoid.maximumRadius) * 4.0;
+
+        // When the tile width is greater than maxTileWidth as computed above, the error
+        // of a normal fill tile from globe curvature alone will exceed the allowed geometric
+        // error. Terrain won't change that much. However, we can allow more error than that.
+        // A little blockiness during load is acceptable. For the WGS84 ellipsoid and
+        // standard geometric error setup, the value here will have us use a heightmap
+        // at levels 1, 2, and 3.
+        maxTileWidth *= 1.5;
+
+        if (rectangle.width > maxTileWidth && (maximumHeight - minimumHeight) <= geometricError) {
+            var terrainData = new HeightmapTerrainData({
+                width: 9,
+                height: 9,
+                buffer: heightmapBuffer,
+                structure: {
+                    // Use the maximum as the constant height so that this tile's skirt
+                    // covers any cracks with adjacent tiles.
+                    heightOffset: maximumHeight
+                }
+            });
+            fill.mesh = terrainData._createMeshSync(tile.tilingScheme, tile.x, tile.y, tile.level, 1.0);
+        } else {
+            var encoding = new TerrainEncoding(undefined, undefined, undefined, undefined, true, true);
+
+            var centerCartographic = centerCartographicScratch;
+            centerCartographic.longitude = (rectangle.east + rectangle.west) * 0.5;
+            centerCartographic.latitude = (rectangle.north + rectangle.south) * 0.5;
+            centerCartographic.height = middleHeight;
+            encoding.center = ellipsoid.cartographicToCartesian(centerCartographic, encoding.center);
+
+            // At _most_, we have vertices for the 4 corners, plus 1 center, plus every adjacent edge vertex.
+            // In reality there will be less most of the time, but close enough; better
+            // to overestimate than to re-allocate/copy/traverse the vertices twice.
+            // Also, we'll often be able to squeeze the index data into the extra space in the buffer.
+            var maxVertexCount = 5;
+            var meshes;
+
+            meshes = fill.westMeshes;
+            for (i = 0, len = meshes.length; i < len; ++i) {
+                maxVertexCount += meshes[i].eastIndicesNorthToSouth.length;
+            }
+
+            meshes = fill.southMeshes;
+            for (i = 0, len = meshes.length; i < len; ++i) {
+                maxVertexCount += meshes[i].northIndicesWestToEast.length;
+            }
+
+            meshes = fill.eastMeshes;
+            for (i = 0, len = meshes.length; i < len; ++i) {
+                maxVertexCount += meshes[i].westIndicesSouthToNorth.length;
+            }
+
+            meshes = fill.northMeshes;
+            for (i = 0, len = meshes.length; i < len; ++i) {
+                maxVertexCount += meshes[i].southIndicesEastToWest.length;
+            }
+
+            var heightRange = heightRangeScratch;
+            heightRange.minimumHeight = minimumHeight;
+            heightRange.maximumHeight = maximumHeight;
+
+            var stride = encoding.getStride();
+            var typedArray = new Float32Array(maxVertexCount * stride);
+
+            var nextIndex = 0;
+            var northwestIndex = nextIndex;
+            nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 1.0, nwCorner.height, nwCorner.encodedNormal, 1.0, heightRange);
+            nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.westTiles, fill.westMeshes, TileEdge.EAST, heightRange);
+            var southwestIndex = nextIndex;
+            nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 0.0, 0.0, swCorner.height, swCorner.encodedNormal, 0.0, heightRange);
+            nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.southTiles, fill.southMeshes, TileEdge.NORTH, heightRange);
+            var southeastIndex = nextIndex;
+            nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 0.0, seCorner.height, seCorner.encodedNormal, 0.0, heightRange);
+            nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.eastTiles, fill.eastMeshes, TileEdge.WEST, heightRange);
+            var northeastIndex = nextIndex;
+            nextIndex = addVertexWithComputedPosition(ellipsoid, rectangle, encoding, typedArray, nextIndex, 1.0, 1.0, neCorner.height, neCorner.encodedNormal, 1.0, heightRange);
+            nextIndex = addEdge(fill, ellipsoid, encoding, typedArray, nextIndex, fill.northTiles, fill.northMeshes, TileEdge.SOUTH, heightRange);
+
+            minimumHeight = heightRange.minimumHeight;
+            maximumHeight = heightRange.maximumHeight;
+
+            var obb = OrientedBoundingBox.fromRectangle(rectangle, minimumHeight, maximumHeight, tile.tilingScheme.ellipsoid);
+
+            // Add a single vertex at the center of the tile.
+            var southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.south);
+            var oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(rectangle.north) - southMercatorY);
+            var centerWebMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(centerCartographic.latitude) - southMercatorY) * oneOverMercatorHeight;
+
+            ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, normalScratch);
+            var centerEncodedNormal = AttributeCompression.octEncode(normalScratch, octEncodedNormalScratch);
+
+            var centerIndex = nextIndex;
+            encoding.encode(typedArray, nextIndex * stride, obb.center, Cartesian2.fromElements(0.5, 0.5, uvScratch), middleHeight, centerEncodedNormal, centerWebMercatorT);
+            ++nextIndex;
+
+            var vertexCount = nextIndex;
+
+            var bytesPerIndex = vertexCount < 256 ? 1 : 2;
+            var indexCount = (vertexCount - 1) * 3; // one triangle per edge vertex
+            var indexDataBytes = indexCount * bytesPerIndex;
+            var availableBytesInBuffer = (typedArray.length - vertexCount * stride) * Float32Array.BYTES_PER_ELEMENT;
+
+            var indices;
+            if (availableBytesInBuffer >= indexDataBytes) {
+                // Store the index data in the same buffer as the vertex data.
+                var startIndex = vertexCount * stride * Float32Array.BYTES_PER_ELEMENT;
+                indices = vertexCount < 256
+                    ? new Uint8Array(typedArray.buffer, startIndex, indexCount)
+                    : new Uint16Array(typedArray.buffer, startIndex, indexCount);
+            } else {
+                // Allocate a new buffer for the index data.
+                indices = vertexCount < 256 ? new Uint8Array(indexCount) : new Uint16Array(indexCount);
+            }
+
+            typedArray = new Float32Array(typedArray.buffer, 0, vertexCount * stride);
+
+            var indexOut = 0;
+            for (i = 0; i < vertexCount - 2; ++i) {
+                indices[indexOut++] = centerIndex;
+                indices[indexOut++] = i;
+                indices[indexOut++] = i + 1;
+            }
+
+            indices[indexOut++] = centerIndex;
+            indices[indexOut++] = i;
+            indices[indexOut++] = 0;
+
+            var westIndicesSouthToNorth = [];
+            for (i = southwestIndex; i >= northwestIndex; --i) {
+                westIndicesSouthToNorth.push(i);
+            }
+
+            var southIndicesEastToWest = [];
+            for (i = southeastIndex; i >= southwestIndex; --i) {
+                southIndicesEastToWest.push(i);
+            }
+
+            var eastIndicesNorthToSouth = [];
+            for (i = northeastIndex; i >= southeastIndex; --i) {
+                eastIndicesNorthToSouth.push(i);
+            }
+
+            var northIndicesWestToEast = [];
+            northIndicesWestToEast.push(0);
+            for (i = centerIndex - 1; i >= northeastIndex; --i) {
+                northIndicesWestToEast.push(i);
+            }
+
+            fill.mesh = new TerrainMesh(
+                encoding.center,
+                typedArray,
+                indices,
+                minimumHeight,
+                maximumHeight,
+                BoundingSphere.fromOrientedBoundingBox(obb),
+                computeOccludeePoint(tileProvider, obb.center, rectangle, maximumHeight),
+                encoding.getStride(),
+                obb,
+                encoding,
+                frameState.terrainExaggeration,
+                westIndicesSouthToNorth,
+                southIndicesEastToWest,
+                eastIndicesNorthToSouth,
+                northIndicesWestToEast
+            );
+        }
+
+        var context = frameState.context;
+
+        if (defined(fill.vertexArray)) {
+            if (defined(vertexArraysToDestroy)) {
+                vertexArraysToDestroy.push(fill.vertexArray);
+            } else {
+                GlobeSurfaceTile._freeVertexArray(fill.vertexArray);
+            }
+        }
+
+        fill.vertexArray = GlobeSurfaceTile._createVertexArrayForMesh(context, fill.mesh);
+        surfaceTile.processImagery(tile, tileProvider.terrainProvider, frameState, true);
+
+        var oldTexture = fill.waterMaskTexture;
+        fill.waterMaskTexture = undefined;
+
+        if (tileProvider.terrainProvider.hasWaterMask) {
+            var waterSourceTile = surfaceTile._findAncestorTileWithTerrainData(tile);
+            if (defined(waterSourceTile) && defined(waterSourceTile.data.waterMaskTexture)) {
+                fill.waterMaskTexture = waterSourceTile.data.waterMaskTexture;
+                ++fill.waterMaskTexture.referenceCount;
+                surfaceTile._computeWaterMaskTranslationAndScale(tile, waterSourceTile, fill.waterMaskTranslationAndScale);
+            }
+        }
+
+        if (defined(oldTexture)) {
+            --oldTexture.referenceCount;
+            if (oldTexture.referenceCount === 0) {
+                oldTexture.destroy();
+            }
+        }
+    }
+
+    function addVertexWithComputedPosition(ellipsoid, rectangle, encoding, buffer, index, u, v, height, encodedNormal, webMercatorT, heightRange) {
+        var cartographic = cartographicScratch;
+        cartographic.longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u);
+        cartographic.latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v);
+        cartographic.height = height;
+        var position = ellipsoid.cartographicToCartesian(cartographic, cartesianScratch);
+
+        var uv = uvScratch2;
+        uv.x = u;
+        uv.y = v;
+
+        encoding.encode(buffer, index * encoding.getStride(), position, uv, height, encodedNormal, webMercatorT);
+
+        heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height);
+        heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height);
+
+        return index + 1;
+    }
+
+    var sourceRectangleScratch = new Rectangle();
+
+    function transformTextureCoordinates(sourceTile, targetTile, coordinates, result) {
+        var sourceRectangle = sourceTile.rectangle;
+        var targetRectangle = targetTile.rectangle;
+
+        // Handle transforming across the anti-meridian.
+        if (targetTile.x === 0 && coordinates.x === 1.0 && sourceTile.x === sourceTile.tilingScheme.getNumberOfXTilesAtLevel(sourceTile.level) - 1) {
+            sourceRectangle = Rectangle.clone(sourceTile.rectangle, sourceRectangleScratch);
+            sourceRectangle.west -= CesiumMath.TWO_PI;
+            sourceRectangle.east -= CesiumMath.TWO_PI;
+        } else if (sourceTile.x === 0 && coordinates.x === 0.0 && targetTile.x === targetTile.tilingScheme.getNumberOfXTilesAtLevel(targetTile.level) - 1) {
+            sourceRectangle = Rectangle.clone(sourceTile.rectangle, sourceRectangleScratch);
+            sourceRectangle.west += CesiumMath.TWO_PI;
+            sourceRectangle.east += CesiumMath.TWO_PI;
+        }
+
+        var sourceWidth = sourceRectangle.east - sourceRectangle.west;
+        var umin = (targetRectangle.west - sourceRectangle.west) / sourceWidth;
+        var umax = (targetRectangle.east - sourceRectangle.west) / sourceWidth;
+
+        var sourceHeight = sourceRectangle.north - sourceRectangle.south;
+        var vmin = (targetRectangle.south - sourceRectangle.south) / sourceHeight;
+        var vmax = (targetRectangle.north - sourceRectangle.south) / sourceHeight;
+
+        var u = (coordinates.x - umin) / (umax - umin);
+        var v = (coordinates.y - vmin) / (vmax - vmin);
+
+        // Ensure that coordinates very near the corners are at the corners.
+        if (Math.abs(u) < Math.EPSILON5) {
+            u = 0.0;
+        } else if (Math.abs(u - 1.0) < Math.EPSILON5) {
+            u = 1.0;
+        }
+
+        if (Math.abs(v) < Math.EPSILON5) {
+            v = 0.0;
+        } else if (Math.abs(v - 1.0) < Math.EPSILON5) {
+            v = 1.0;
+        }
+
+        result.x = u;
+        result.y = v;
+        return result;
+    }
+
+    var encodedNormalScratch = new Cartesian2();
+
+    function getVertexFromTileAtCorner(sourceMesh, sourceIndex, u, v, vertex) {
+        var sourceEncoding = sourceMesh.encoding;
+        var sourceVertices = sourceMesh.vertices;
+
+        vertex.height = sourceEncoding.decodeHeight(sourceVertices, sourceIndex);
+
+        if (sourceEncoding.hasVertexNormals) {
+            sourceEncoding.getOctEncodedNormal(sourceVertices, sourceIndex, vertex.encodedNormal);
+        } else {
+            var normal = vertex.encodedNormal;
+            normal.x = 0.0;
+            normal.y = 0.0;
+        }
+    }
+
+    var encodedNormalScratch2 = new Cartesian2();
+    var cartesianScratch2 = new Cartesian3();
+
+    function getInterpolatedVertexAtCorner(ellipsoid, sourceTile, targetTile, sourceMesh, previousIndex, nextIndex, u, v, interpolateU, vertex) {
+        var sourceEncoding = sourceMesh.encoding;
+        var sourceVertices = sourceMesh.vertices;
+
+        var previousUv = transformTextureCoordinates(sourceTile, targetTile, sourceEncoding.decodeTextureCoordinates(sourceVertices, previousIndex, uvScratch), uvScratch);
+        var nextUv = transformTextureCoordinates(sourceTile, targetTile, sourceEncoding.decodeTextureCoordinates(sourceVertices, nextIndex, uvScratch2), uvScratch2);
+
+        var ratio;
+        if (interpolateU) {
+            ratio = (u - previousUv.x) / (nextUv.x - previousUv.x);
+        } else {
+            ratio = (v - previousUv.y) / (nextUv.y - previousUv.y);
+        }
+
+        var height1 = sourceEncoding.decodeHeight(sourceVertices, previousIndex);
+        var height2 = sourceEncoding.decodeHeight(sourceVertices, nextIndex);
+
+        var targetRectangle = targetTile.rectangle;
+        cartographicScratch.longitude = CesiumMath.lerp(targetRectangle.west, targetRectangle.east, u);
+        cartographicScratch.latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v);
+        vertex.height = cartographicScratch.height = CesiumMath.lerp(height1, height2, ratio);
+
+        var normal;
+        if (sourceEncoding.hasVertexNormals) {
+            var encodedNormal1 = sourceEncoding.getOctEncodedNormal(sourceVertices, previousIndex, encodedNormalScratch);
+            var encodedNormal2 = sourceEncoding.getOctEncodedNormal(sourceVertices, nextIndex, encodedNormalScratch2);
+            var normal1 = AttributeCompression.octDecode(encodedNormal1.x, encodedNormal1.y, cartesianScratch);
+            var normal2 = AttributeCompression.octDecode(encodedNormal2.x, encodedNormal2.y, cartesianScratch2);
+            normal = Cartesian3.lerp(normal1, normal2, ratio, cartesianScratch);
+            Cartesian3.normalize(normal, normal);
+            AttributeCompression.octEncode(normal, vertex.encodedNormal);
+        } else {
+            normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch);
+            AttributeCompression.octEncode(normal, vertex.encodedNormal);
+        }
+    }
+
+    function getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex) {
+        vertex.height = height;
+        var normal = ellipsoid.geodeticSurfaceNormalCartographic(cartographicScratch, cartesianScratch);
+        AttributeCompression.octEncode(normal, vertex.encodedNormal);
+    }
+
+    function getCorner(
+        terrainFillMesh,
+        ellipsoid,
+        u, v,
+        cornerTile, cornerMesh,
+        previousEdgeTiles, previousEdgeMeshes,
+        nextEdgeTiles, nextEdgeMeshes,
+        vertex
+    ) {
+        var gotCorner =
+            getCornerFromEdge(terrainFillMesh, ellipsoid, previousEdgeMeshes, previousEdgeTiles, false, u, v, vertex) ||
+            getCornerFromEdge(terrainFillMesh, ellipsoid, nextEdgeMeshes, nextEdgeTiles, true, u, v, vertex);
+        if (gotCorner) {
+            return vertex;
+        }
+
+        var vertexIndex;
+
+        if (meshIsUsable(cornerTile, cornerMesh)) {
+            // Corner mesh is valid, copy its corner vertex to this mesh.
+            if (u === 0.0) {
+                if (v === 0.0) {
+                    // southwest destination, northeast source
+                    vertexIndex = cornerMesh.eastIndicesNorthToSouth[0];
+                } else {
+                    // northwest destination, southeast source
+                    vertexIndex = cornerMesh.southIndicesEastToWest[0];
+                }
+            } else if (v === 0.0) {
+                // southeast destination, northwest source
+                vertexIndex = cornerMesh.northIndicesWestToEast[0];
+            } else {
+                // northeast destination, southwest source
+                vertexIndex = cornerMesh.westIndicesSouthToNorth[0];
+            }
+            getVertexFromTileAtCorner(cornerMesh, vertexIndex, u, v, vertex);
+            return vertex;
+        }
+
+        // There is no precise vertex available from the corner or from either adjacent edge.
+        // This is either because there are no tiles at all at the edges and corner, or
+        // because the tiles at the edge are higher-level-number and don't extend all the way
+        // to the corner.
+        // Try to grab a height from the adjacent edges.
+        var height;
+        if (u === 0.0) {
+            if (v === 0.0) {
+                // southwest
+                height = getClosestHeightToCorner(
+                    terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST,
+                    terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH,
+                    u, v);
+            } else {
+                // northwest
+                height = getClosestHeightToCorner(
+                    terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH,
+                    terrainFillMesh.westMeshes, terrainFillMesh.westTiles, TileEdge.EAST,
+                    u, v);
+            }
+        } else if (v === 0.0) {
+            // southeast
+            height = getClosestHeightToCorner(
+                terrainFillMesh.southMeshes, terrainFillMesh.southTiles, TileEdge.NORTH,
+                terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST,
+                u, v);
+        } else {
+            // northeast
+            height = getClosestHeightToCorner(
+                terrainFillMesh.eastMeshes, terrainFillMesh.eastTiles, TileEdge.WEST,
+                terrainFillMesh.northMeshes, terrainFillMesh.northTiles, TileEdge.SOUTH,
+                u, v);
+        }
+
+        if (defined(height)) {
+            getVertexWithHeightAtCorner(terrainFillMesh, ellipsoid, u, v, height, vertex);
+            return vertex;
+        }
+
+        // No heights available that are closer than the adjacent corners.
+        return undefined;
+    }
+
+    function getClosestHeightToCorner(
+        previousMeshes, previousTiles, previousEdge,
+        nextMeshes, nextTiles, nextEdge,
+        u, v
+    ) {
+        var height1 = getNearestHeightOnEdge(previousMeshes, previousTiles, false, previousEdge, u, v);
+        var height2 = getNearestHeightOnEdge(nextMeshes, nextTiles, true, nextEdge, u, v);
+        if (defined(height1) && defined(height2)) {
+            // It would be slightly better to do a weighted average of the two heights
+            // based on their distance from the corner, but it shouldn't matter much in practice.
+            return (height1 + height2) * 0.5;
+        } else if (defined(height1)) {
+            return height1;
+        }
+        return height2;
+    }
+
+    function addEdge(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles, edgeMeshes, tileEdge, heightRange) {
+        for (var i = 0; i < edgeTiles.length; ++i) {
+            nextIndex = addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTiles[i], edgeMeshes[i], tileEdge, heightRange);
+        }
+        return nextIndex;
+    }
+
+    function addEdgeMesh(terrainFillMesh, ellipsoid, encoding, typedArray, nextIndex, edgeTile, edgeMesh, tileEdge, heightRange) {
+        // Handle copying edges across the anti-meridian.
+        var sourceRectangle = edgeTile.rectangle;
+        if (tileEdge === TileEdge.EAST && terrainFillMesh.tile.x === 0) {
+            sourceRectangle = Rectangle.clone(edgeTile.rectangle, sourceRectangleScratch);
+            sourceRectangle.west -= CesiumMath.TWO_PI;
+            sourceRectangle.east -= CesiumMath.TWO_PI;
+        } else if (tileEdge === TileEdge.WEST && edgeTile.x === 0) {
+            sourceRectangle = Rectangle.clone(edgeTile.rectangle, sourceRectangleScratch);
+            sourceRectangle.west += CesiumMath.TWO_PI;
+            sourceRectangle.east += CesiumMath.TWO_PI;
+        }
+
+        var targetRectangle = terrainFillMesh.tile.rectangle;
+
+        var lastU;
+        var lastV;
+
+        if (nextIndex > 0) {
+            encoding.decodeTextureCoordinates(typedArray, nextIndex - 1, uvScratch);
+            lastU = uvScratch.x;
+            lastV = uvScratch.y;
+        }
+
+        var indices;
+        var compareU;
+
+        switch (tileEdge) {
+            case TileEdge.WEST:
+                indices = edgeMesh.westIndicesSouthToNorth;
+                compareU = false;
+                break;
+            case TileEdge.NORTH:
+                indices = edgeMesh.northIndicesWestToEast;
+                compareU = true;
+                break;
+            case TileEdge.EAST:
+                indices = edgeMesh.eastIndicesNorthToSouth;
+                compareU = false;
+                break;
+            case TileEdge.SOUTH:
+                indices = edgeMesh.southIndicesEastToWest;
+                compareU = true;
+                break;
+        }
+
+        var sourceTile = edgeTile;
+        var targetTile = terrainFillMesh.tile;
+        var sourceEncoding = edgeMesh.encoding;
+        var sourceVertices = edgeMesh.vertices;
+        var targetStride = encoding.getStride();
+
+        var southMercatorY;
+        var oneOverMercatorHeight;
+        if (sourceEncoding.hasWebMercatorT) {
+            southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(targetRectangle.south);
+            oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(targetRectangle.north) - southMercatorY);
+        }
+
+        for (var i = 0; i < indices.length; ++i) {
+            var index = indices[i];
+
+            var uv = sourceEncoding.decodeTextureCoordinates(sourceVertices, index, uvScratch);
+            transformTextureCoordinates(sourceTile, targetTile, uv, uv);
+            var u = uv.x;
+            var v = uv.y;
+            var uOrV = compareU ? u : v;
+
+            if (uOrV < 0.0 || uOrV > 1.0) {
+                // Vertex is outside the target tile - skip it.
+                continue;
+            }
+
+            if (Math.abs(u - lastU) < CesiumMath.EPSILON5 && Math.abs(v - lastV) < CesiumMath.EPSILON5) {
+                // Vertex is very close to the previous one - skip it.
+                continue;
+            }
+
+            var nearlyEdgeU = Math.abs(u) < CesiumMath.EPSILON5 || Math.abs(u - 1.0) < CesiumMath.EPSILON5;
+            var nearlyEdgeV = Math.abs(v) < CesiumMath.EPSILON5 || Math.abs(v - 1.0) < CesiumMath.EPSILON5;
+
+            if (nearlyEdgeU && nearlyEdgeV) {
+                // Corner vertex - skip it.
+                continue;
+            }
+
+            var position = sourceEncoding.decodePosition(sourceVertices, index, cartesianScratch);
+            var height = sourceEncoding.decodeHeight(sourceVertices, index);
+
+            var normal;
+            if (sourceEncoding.hasVertexNormals) {
+                normal = sourceEncoding.getOctEncodedNormal(sourceVertices, index, octEncodedNormalScratch);
+            } else {
+                normal = octEncodedNormalScratch;
+                normal.x = 0.0;
+                normal.y = 0.0;
+            }
+
+            var webMercatorT = v;
+            if (sourceEncoding.hasWebMercatorT) {
+                var latitude = CesiumMath.lerp(targetRectangle.south, targetRectangle.north, v);
+                webMercatorT = (WebMercatorProjection.geodeticLatitudeToMercatorAngle(latitude) - southMercatorY) * oneOverMercatorHeight;
+            }
+
+            encoding.encode(typedArray, nextIndex * targetStride, position, uv, height, normal, webMercatorT);
+
+            heightRange.minimumHeight = Math.min(heightRange.minimumHeight, height);
+            heightRange.maximumHeight = Math.max(heightRange.maximumHeight, height);
+
+            ++nextIndex;
+        }
+
+        return nextIndex;
+    }
+
+    function getNearestHeightOnEdge(meshes, tiles, isNext, edge, u, v) {
+        var meshStart;
+        var meshEnd;
+        var meshStep;
+
+        if (isNext) {
+            meshStart = 0;
+            meshEnd = meshes.length;
+            meshStep = 1;
+        } else {
+            meshStart = meshes.length - 1;
+            meshEnd = -1;
+            meshStep = -1;
+        }
+
+        for (var meshIndex = meshStart; meshIndex !== meshEnd; meshIndex += meshStep) {
+            var mesh = meshes[meshIndex];
+            var tile = tiles[meshIndex];
+            if (!meshIsUsable(tile, mesh)) {
+                continue;
+            }
+
+            var indices;
+            switch (edge) {
+                case TileEdge.WEST:
+                    indices = mesh.westIndicesSouthToNorth;
+                    break;
+                case TileEdge.SOUTH:
+                    indices = mesh.southIndicesEastToWest;
+                    break;
+                case TileEdge.EAST:
+                    indices = mesh.eastIndicesNorthToSouth;
+                    break;
+                case TileEdge.NORTH:
+                    indices = mesh.northIndicesWestToEast;
+                    break;
+            }
+
+            var index = indices[isNext ? 0 : indices.length - 1];
+            if (defined(index)) {
+                return mesh.encoding.decodeHeight(mesh.vertices, index);
+            }
+        }
+
+        return undefined;
+    }
+
+    function meshIsUsable(tile, mesh) {
+        return defined(mesh) && (!defined(tile.data.fill) || !tile.data.fill.changedThisFrame);
+    }
+
+    function getCornerFromEdge(terrainFillMesh, ellipsoid, edgeMeshes, edgeTiles, isNext, u, v, vertex) {
+        var edgeVertices;
+        var compareU;
+        var increasing;
+        var vertexIndexIndex;
+        var vertexIndex;
+        var sourceTile = edgeTiles[isNext ? 0 : edgeMeshes.length - 1];
+        var sourceMesh = edgeMeshes[isNext ? 0 : edgeMeshes.length - 1];
+
+        if (meshIsUsable(sourceTile, sourceMesh)) {
+            // Previous mesh is valid, but we don't know yet if it covers this corner.
+            if (u === 0.0) {
+                if (v === 0.0) {
+                    // southwest
+                    edgeVertices = isNext ? sourceMesh.northIndicesWestToEast : sourceMesh.eastIndicesNorthToSouth;
+                    compareU = isNext;
+                    increasing = isNext;
+                } else {
+                    // northwest
+                    edgeVertices = isNext ? sourceMesh.eastIndicesNorthToSouth : sourceMesh.southIndicesEastToWest;
+                    compareU = !isNext;
+                    increasing = false;
+                }
+            } else if (v === 0.0) {
+                // southeast
+                edgeVertices = isNext ? sourceMesh.westIndicesSouthToNorth : sourceMesh.northIndicesWestToEast;
+                compareU = !isNext;
+                increasing = true;
+            } else {
+                // northeast
+                edgeVertices = isNext ? sourceMesh.southIndicesEastToWest : sourceMesh.westIndicesSouthToNorth;
+                compareU = isNext;
+                increasing = !isNext;
+            }
+
+            if (edgeVertices.length > 0) {
+                // The vertex we want will very often be the first/last vertex so check that first.
+                vertexIndexIndex = isNext ? 0 : edgeVertices.length - 1;
+                vertexIndex = edgeVertices[vertexIndexIndex];
+                sourceMesh.encoding.decodeTextureCoordinates(sourceMesh.vertices, vertexIndex, uvScratch);
+                var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch);
+                if (targetUv.x === u && targetUv.y === v) {
+                    // Vertex is good!
+                    getVertexFromTileAtCorner(sourceMesh, vertexIndex, u, v, vertex);
+                    return true;
+                }
+
+                // The last vertex is not the one we need, try binary searching for the right one.
+                vertexIndexIndex = binarySearch(edgeVertices, compareU ? u : v, function(vertexIndex, textureCoordinate) {
+                    sourceMesh.encoding.decodeTextureCoordinates(sourceMesh.vertices, vertexIndex, uvScratch);
+                    var targetUv = transformTextureCoordinates(sourceTile, terrainFillMesh.tile, uvScratch, uvScratch);
+                    if (increasing) {
+                        if (compareU) {
+                            return targetUv.x - u;
+                        }
+                        return targetUv.y - v;
+                    } else if (compareU) {
+                        return u - targetUv.x;
+                    }
+                    return v - targetUv.y;
+                });
+
+                if (vertexIndexIndex < 0) {
+                    vertexIndexIndex = ~vertexIndexIndex;
+
+                    if (vertexIndexIndex > 0 && vertexIndexIndex < edgeVertices.length) {
+                        // The corner falls between two vertices, so interpolate between them.
+                        getInterpolatedVertexAtCorner(ellipsoid, sourceTile, terrainFillMesh.tile, sourceMesh, edgeVertices[vertexIndexIndex - 1], edgeVertices[vertexIndexIndex], u, v, compareU, vertex);
+                        return true;
+                    }
+                } else {
+                    // Found a vertex that fits in the corner exactly.
+                    getVertexFromTileAtCorner(sourceMesh, edgeVertices[vertexIndexIndex], u, v, vertex);
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];
+
+    function computeOccludeePoint(tileProvider, center, rectangle, height, result) {
+        var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid;
+        var ellipsoid = ellipsoidalOccluder.ellipsoid;
+
+        var cornerPositions = cornerPositionsScratch;
+        Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]);
+        Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]);
+        Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]);
+        Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]);
+
+        return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result);
+    }
+
+    return TerrainFillMesh;
+});
diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js
index 52686d5a3d8..4b1bba4a296 100644
--- a/Source/Scene/TileBoundingRegion.js
+++ b/Source/Scene/TileBoundingRegion.js
@@ -50,6 +50,8 @@ define([
      * @param {Number} [options.minimumHeight=0.0] The minimum height of the region.
      * @param {Number} [options.maximumHeight=0.0] The maximum height of the region.
      * @param {Ellipsoid} [options.ellipsoid=Cesium.Ellipsoid.WGS84] The ellipsoid.
+     * @param {Boolean} [options.computeBoundingVolumes=true] True to compute the {@link TileBoundingRegion#boundingVolume} and
+     *                  {@link TileBoundingVolume#boundingSphere}. If false, these properties will be undefined.
      *
      * @private
      */
@@ -122,10 +124,12 @@ define([
         var ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);
         computeBox(this, options.rectangle, ellipsoid);
 
-        // An oriented bounding box that encloses this tile's region.  This is used to calculate tile visibility.
-        this._orientedBoundingBox = OrientedBoundingBox.fromRectangle(this.rectangle, this.minimumHeight, this.maximumHeight, ellipsoid);
+        if (defaultValue(options.computeBoundingVolumes, true)) {
+            // An oriented bounding box that encloses this tile's region.  This is used to calculate tile visibility.
+            this._orientedBoundingBox = OrientedBoundingBox.fromRectangle(this.rectangle, this.minimumHeight, this.maximumHeight, ellipsoid);
 
-        this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(this._orientedBoundingBox);
+            this._boundingSphere = BoundingSphere.fromOrientedBoundingBox(this._orientedBoundingBox);
+        }
     }
 
     defineProperties(TileBoundingRegion.prototype, {
@@ -300,16 +304,24 @@ define([
         }
 
         var cameraHeight;
+        var minimumHeight;
+        var maximumHeight;
         if (frameState.mode === SceneMode.SCENE3D) {
             cameraHeight = cameraCartographicPosition.height;
+            minimumHeight = this.minimumHeight;
+            maximumHeight = this.maximumHeight;
         } else {
             cameraHeight = cameraCartesianPosition.x;
+            minimumHeight = 0.0;
+            maximumHeight = 0.0;
         }
 
-        var maximumHeight = frameState.mode === SceneMode.SCENE3D ? this.maximumHeight : 0.0;
-        var distanceFromTop = cameraHeight - maximumHeight;
-        if (distanceFromTop > 0.0) {
-            result += distanceFromTop * distanceFromTop;
+        if (cameraHeight > maximumHeight) {
+            var distanceAboveTop = cameraHeight - maximumHeight;
+            result += distanceAboveTop * distanceAboveTop;
+        } else if (cameraHeight < minimumHeight) {
+            var distanceBelowBottom = minimumHeight - cameraHeight;
+            result += distanceBelowBottom * distanceBelowBottom;
         }
 
         return Math.sqrt(result);
diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js
index 592ca81550f..11817176aa9 100644
--- a/Source/Scene/TileImagery.js
+++ b/Source/Scene/TileImagery.js
@@ -43,13 +43,16 @@ define([
      *
      * @param {Tile} tile The tile to which this instance belongs.
      * @param {FrameState} frameState The frameState.
+     * @param {Boolean} skipLoading True to skip loading, e.g. new requests, creating textures. This function will
+     *                  still synchronously process imagery that's already mostly ready to go, e.g. use textures
+     *                  already loaded on ancestor tiles.
      * @returns {Boolean} True if this instance is done loading; otherwise, false.
      */
-    TileImagery.prototype.processStateMachine = function(tile, frameState) {
+    TileImagery.prototype.processStateMachine = function(tile, frameState, skipLoading) {
         var loadingImagery = this.loadingImagery;
         var imageryLayer = loadingImagery.imageryLayer;
 
-        loadingImagery.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction);
+        loadingImagery.processStateMachine(frameState, !this.useWebMercatorT, skipLoading);
 
         if (loadingImagery.state === ImageryState.READY) {
             if (defined(this.readyImagery)) {
@@ -91,7 +94,7 @@ define([
                 // Push the ancestor's load process along a bit.  This is necessary because some ancestor imagery
                 // tiles may not be attached directly to a terrain tile.  Such tiles will never load if
                 // we don't do it here.
-                closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction);
+                closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT, skipLoading);
                 return false; // not done loading
             }
             // This imagery tile is failed or invalid, and we have the "best available" substitute.
diff --git a/Source/Scene/TileSelectionResult.js b/Source/Scene/TileSelectionResult.js
new file mode 100644
index 00000000000..46637e37584
--- /dev/null
+++ b/Source/Scene/TileSelectionResult.js
@@ -0,0 +1,86 @@
+define([
+    ], function() {
+    'use strict';
+
+    /**
+     * Indicates what happened the last time this tile was visited for selection.
+     * @private
+     */
+    var TileSelectionResult = {
+        /**
+         * There was no selection result, perhaps because the tile wasn't visited
+         * last frame.
+         */
+        NONE: 0,
+
+        /**
+         * This tile was deemed not visible and culled.
+         */
+        CULLED: 1,
+
+        /**
+         * The tile was selected for rendering.
+         */
+        RENDERED: 2,
+
+        /**
+         * This tile did not meet the required screen-space error and was refined.
+         */
+        REFINED: 3,
+
+        /**
+         * This tile was originally rendered, but it got kicked out of the render list
+         * in favor of an ancestor because it is not yet renderable.
+         */
+        RENDERED_AND_KICKED: 2 | 4,
+
+        /**
+         * This tile was originally refined, but its rendered descendants got kicked out of the
+         * render list in favor of an ancestor because it is not yet renderable.
+         */
+        REFINED_AND_KICKED: 3 | 4,
+
+        /**
+         * This tile was culled because it was not visible, but it still needs to be loaded
+         * and any heights on it need to be updated because the camera's position or the
+         * camera's reference frame's origin falls inside this tile. Loading this tile
+         * could affect the position of the camera if the camera is currently below
+         * terrain or if it is tracking an object whose height is referenced to terrain.
+         * And a change in the camera position may, in turn, affect what is culled.
+         */
+        CULLED_BUT_NEEDED: 1 | 8,
+
+        /**
+         * Determines if a selection result indicates that this tile or its descendants were
+         * kicked from the render list. In other words, if it is <code>RENDERED_AND_KICKED</code>
+         * or <code>REFINED_AND_KICKED</code>.
+         *
+         * @param {TileSelectionResult} value The selection result to test.
+         * @returns {Boolean} true if the tile was kicked, no matter if it was originally rendered or refined.
+         */
+        wasKicked: function(value) {
+            return value >= TileSelectionResult.RENDERED_AND_KICKED;
+        },
+
+        /**
+         * Determines the original selection result prior to being kicked or CULLED_BUT_NEEDED.
+         * If the tile wasn't kicked or CULLED_BUT_NEEDED, the original value is returned.
+         * @param {TileSelectionResult} value The selection result.
+         * @returns {TileSelectionResult} The original selection result prior to kicking.
+         */
+        originalResult: function(value) {
+            return value & 3;
+        },
+
+        /**
+         * Converts this selection result to a kick.
+         * @param {TileSelectionResult} value The original selection result.
+         * @returns {TileSelectionResult} The kicked form of the selection result.
+         */
+        kick: function(value) {
+            return value | 4;
+        }
+    };
+
+    return TileSelectionResult;
+});
diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js
deleted file mode 100644
index 450590d70c7..00000000000
--- a/Source/Scene/TileTerrain.js
+++ /dev/null
@@ -1,266 +0,0 @@
-define([
-        '../Core/BoundingSphere',
-        '../Core/Cartesian3',
-        '../Core/defined',
-        '../Core/DeveloperError',
-        '../Core/IndexDatatype',
-        '../Core/OrientedBoundingBox',
-        '../Core/Request',
-        '../Core/RequestState',
-        '../Core/RequestType',
-        '../Core/TileProviderError',
-        '../Renderer/Buffer',
-        '../Renderer/BufferUsage',
-        '../Renderer/VertexArray',
-        '../ThirdParty/when',
-        './TerrainState'
-    ], function(
-        BoundingSphere,
-        Cartesian3,
-        defined,
-        DeveloperError,
-        IndexDatatype,
-        OrientedBoundingBox,
-        Request,
-        RequestState,
-        RequestType,
-        TileProviderError,
-        Buffer,
-        BufferUsage,
-        VertexArray,
-        when,
-        TerrainState) {
-    'use strict';
-
-    /**
-     * Manages details of the terrain load or upsample process.
-     *
-     * @alias TileTerrain
-     * @constructor
-     * @private
-     *
-     * @param {TerrainData} [upsampleDetails.data] The terrain data being upsampled.
-     * @param {Number} [upsampleDetails.x] The X coordinate of the tile being upsampled.
-     * @param {Number} [upsampleDetails.y] The Y coordinate of the tile being upsampled.
-     * @param {Number} [upsampleDetails.level] The level coordinate of the tile being upsampled.
-     */
-    function TileTerrain(upsampleDetails) {
-        /**
-         * The current state of the terrain in the terrain processing pipeline.
-         * @type {TerrainState}
-         * @default {@link TerrainState.UNLOADED}
-         */
-        this.state = TerrainState.UNLOADED;
-        this.data = undefined;
-        this.mesh = undefined;
-        this.vertexArray = undefined;
-        this.upsampleDetails = upsampleDetails;
-        this.request = undefined;
-    }
-
-    TileTerrain.prototype.freeResources = function() {
-        this.state = TerrainState.UNLOADED;
-        this.data = undefined;
-        this.mesh = undefined;
-
-        if (defined(this.vertexArray)) {
-            var indexBuffer = this.vertexArray.indexBuffer;
-
-            this.vertexArray.destroy();
-            this.vertexArray = undefined;
-
-            if (!indexBuffer.isDestroyed() && defined(indexBuffer.referenceCount)) {
-                --indexBuffer.referenceCount;
-                if (indexBuffer.referenceCount === 0) {
-                    indexBuffer.destroy();
-                }
-            }
-        }
-    };
-
-    TileTerrain.prototype.publishToTile = function(tile) {
-        var surfaceTile = tile.data;
-
-        var mesh = this.mesh;
-        Cartesian3.clone(mesh.center, surfaceTile.center);
-        surfaceTile.minimumHeight = mesh.minimumHeight;
-        surfaceTile.maximumHeight = mesh.maximumHeight;
-        surfaceTile.boundingSphere3D = BoundingSphere.clone(mesh.boundingSphere3D, surfaceTile.boundingSphere3D);
-        surfaceTile.orientedBoundingBox = OrientedBoundingBox.clone(mesh.orientedBoundingBox, surfaceTile.orientedBoundingBox);
-        surfaceTile.tileBoundingRegion.minimumHeight = mesh.minimumHeight;
-        surfaceTile.tileBoundingRegion.maximumHeight = mesh.maximumHeight;
-        tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace);
-    };
-
-    TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, priorityFunction) {
-        if (this.state === TerrainState.UNLOADED) {
-            requestTileGeometry(this, terrainProvider, x, y, level, priorityFunction);
-        }
-
-        if (this.state === TerrainState.RECEIVED) {
-            transform(this, frameState, terrainProvider, x, y, level);
-        }
-
-        if (this.state === TerrainState.TRANSFORMED) {
-            createResources(this, frameState.context, terrainProvider, x, y, level);
-        }
-    };
-
-    function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, priorityFunction) {
-        function success(terrainData) {
-            tileTerrain.data = terrainData;
-            tileTerrain.state = TerrainState.RECEIVED;
-            tileTerrain.request = undefined;
-        }
-
-        function failure() {
-            if (tileTerrain.request.state === RequestState.CANCELLED) {
-                // Cancelled due to low priority - try again later.
-                tileTerrain.data = undefined;
-                tileTerrain.state = TerrainState.UNLOADED;
-                tileTerrain.request = undefined;
-                return;
-            }
-
-            // Initially assume failure.  handleError may retry, in which case the state will
-            // change to RECEIVING or UNLOADED.
-            tileTerrain.state = TerrainState.FAILED;
-            tileTerrain.request = undefined;
-
-            var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.';
-            terrainProvider._requestError = TileProviderError.handleError(
-                terrainProvider._requestError,
-                terrainProvider,
-                terrainProvider.errorEvent,
-                message,
-                x, y, level,
-                doRequest);
-        }
-
-        function doRequest() {
-            // Request the terrain from the terrain provider.
-            var request = new Request({
-                throttle : true,
-                throttleByServer : true,
-                type : RequestType.TERRAIN,
-                priorityFunction : priorityFunction
-            });
-            tileTerrain.request = request;
-            tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level, request);
-
-            // If the request method returns undefined (instead of a promise), the request
-            // has been deferred.
-            if (defined(tileTerrain.data)) {
-                tileTerrain.state = TerrainState.RECEIVING;
-                when(tileTerrain.data, success, failure);
-            } else {
-                // Deferred - try again later.
-                tileTerrain.state = TerrainState.UNLOADED;
-                tileTerrain.request = undefined;
-            }
-        }
-
-        doRequest();
-    }
-
-    TileTerrain.prototype.processUpsampleStateMachine = function(frameState, terrainProvider, x, y, level) {
-        if (this.state === TerrainState.UNLOADED) {
-            var upsampleDetails = this.upsampleDetails;
-
-            //>>includeStart('debug', pragmas.debug);
-            if (!defined(upsampleDetails)) {
-                throw new DeveloperError('TileTerrain cannot upsample unless provided upsampleDetails.');
-            }
-            //>>includeEnd('debug');
-
-            var sourceData = upsampleDetails.data;
-            var sourceX = upsampleDetails.x;
-            var sourceY = upsampleDetails.y;
-            var sourceLevel = upsampleDetails.level;
-
-            this.data = sourceData.upsample(terrainProvider.tilingScheme, sourceX, sourceY, sourceLevel, x, y, level);
-            if (!defined(this.data)) {
-                // The upsample request has been deferred - try again later.
-                return;
-            }
-
-            this.state = TerrainState.RECEIVING;
-
-            var that = this;
-            when(this.data, function(terrainData) {
-                that.data = terrainData;
-                that.state = TerrainState.RECEIVED;
-            }, function() {
-                that.state = TerrainState.FAILED;
-            });
-        }
-
-        if (this.state === TerrainState.RECEIVED) {
-            transform(this, frameState, terrainProvider, x, y, level);
-        }
-
-        if (this.state === TerrainState.TRANSFORMED) {
-            createResources(this, frameState.context, terrainProvider, x, y, level);
-        }
-    };
-
-    function transform(tileTerrain, frameState, terrainProvider, x, y, level) {
-        var tilingScheme = terrainProvider.tilingScheme;
-
-        var terrainData = tileTerrain.data;
-        var meshPromise = terrainData.createMesh(tilingScheme, x, y, level, frameState.terrainExaggeration);
-
-        if (!defined(meshPromise)) {
-            // Postponed.
-            return;
-        }
-
-        tileTerrain.state = TerrainState.TRANSFORMING;
-
-        when(meshPromise, function(mesh) {
-            tileTerrain.mesh = mesh;
-            tileTerrain.state = TerrainState.TRANSFORMED;
-        }, function() {
-            tileTerrain.state = TerrainState.FAILED;
-        });
-    }
-
-    function createResources(tileTerrain, context, terrainProvider, x, y, level) {
-        var typedArray = tileTerrain.mesh.vertices;
-        var buffer = Buffer.createVertexBuffer({
-            context : context,
-            typedArray : typedArray,
-            usage : BufferUsage.STATIC_DRAW
-        });
-        var attributes = tileTerrain.mesh.encoding.getAttributes(buffer);
-
-        var indexBuffers = tileTerrain.mesh.indices.indexBuffers || {};
-        var indexBuffer = indexBuffers[context.id];
-        if (!defined(indexBuffer) || indexBuffer.isDestroyed()) {
-            var indices = tileTerrain.mesh.indices;
-            var indexDatatype = (indices.BYTES_PER_ELEMENT === 2) ?  IndexDatatype.UNSIGNED_SHORT : IndexDatatype.UNSIGNED_INT;
-            indexBuffer = Buffer.createIndexBuffer({
-                context : context,
-                typedArray : indices,
-                usage : BufferUsage.STATIC_DRAW,
-                indexDatatype : indexDatatype
-            });
-            indexBuffer.vertexArrayDestroyable = false;
-            indexBuffer.referenceCount = 1;
-            indexBuffers[context.id] = indexBuffer;
-            tileTerrain.mesh.indices.indexBuffers = indexBuffers;
-        } else {
-            ++indexBuffer.referenceCount;
-        }
-
-        tileTerrain.vertexArray = new VertexArray({
-            context : context,
-            attributes : attributes,
-            indexBuffer : indexBuffer
-        });
-
-        tileTerrain.state = TerrainState.READY;
-    }
-
-    return TileTerrain;
-});
diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl
index 1558f577ee6..9559b43715d 100644
--- a/Source/Shaders/GlobeFS.glsl
+++ b/Source/Shaders/GlobeFS.glsl
@@ -76,6 +76,10 @@ uniform float u_minimumBrightness;
 uniform vec3 u_hsbShift; // Hue, saturation, brightness
 #endif
 
+#ifdef HIGHLIGHT_FILL_TILE
+uniform vec4 u_fillHighlightColor;
+#endif
+
 varying vec3 v_positionMC;
 varying vec3 v_positionEC;
 varying vec3 v_textureCoordinates;
@@ -194,7 +198,6 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat
 
 void main()
 {
-
 #ifdef TILE_LIMIT_RECTANGLE
     if (v_textureCoordinates.x < u_cartographicLimitRectangle.x || u_cartographicLimitRectangle.z < v_textureCoordinates.x ||
         v_textureCoordinates.y < u_cartographicLimitRectangle.y || u_cartographicLimitRectangle.w < v_textureCoordinates.y)
@@ -307,6 +310,10 @@ void main()
     }
 #endif
 
+#ifdef HIGHLIGHT_FILL_TILE
+    finalColor = vec4(mix(finalColor.rgb, u_fillHighlightColor.rgb, u_fillHighlightColor.a), finalColor.a);
+#endif
+
 #if defined(FOG) || defined(GROUND_ATMOSPHERE)
     vec3 fogColor = colorCorrect(v_fogMieColor) + finalColor.rgb * colorCorrect(v_fogRayleighColor);
 #ifndef HDR
diff --git a/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js b/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js
index cbde530e722..b86ed3fbd43 100644
--- a/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js
+++ b/Source/Workers/createVerticesFromGoogleEarthEnterpriseBuffer.js
@@ -79,7 +79,11 @@ define([
             occludeePointInScaledSpace : statistics.occludeePointInScaledSpace,
             encoding : statistics.encoding,
             vertexCountWithoutSkirts : statistics.vertexCountWithoutSkirts,
-            skirtIndex : statistics.skirtIndex
+            skirtIndex : statistics.skirtIndex,
+            westIndicesSouthToNorth : statistics.westIndicesSouthToNorth,
+            southIndicesEastToWest : statistics.southIndicesEastToWest,
+            eastIndicesNorthToSouth : statistics.eastIndicesNorthToSouth,
+            northIndicesWestToEast : statistics.northIndicesWestToEast
         };
     }
 
@@ -398,6 +402,17 @@ define([
             bufferIndex = encoding.encode(vertices, bufferIndex, positions[k], uvs[k], heights[k], undefined, webMercatorTs[k]);
         }
 
+        var westIndicesSouthToNorth = westBorder.map(function(vertex) { return vertex.index; }).reverse();
+        var southIndicesEastToWest = southBorder.map(function(vertex) { return vertex.index; }).reverse();
+        var eastIndicesNorthToSouth = eastBorder.map(function(vertex) { return vertex.index; }).reverse();
+        var northIndicesWestToEast = northBorder.map(function(vertex) { return vertex.index; }).reverse();
+
+        southIndicesEastToWest.unshift(eastIndicesNorthToSouth[eastIndicesNorthToSouth.length - 1]);
+        southIndicesEastToWest.push(westIndicesSouthToNorth[0]);
+
+        northIndicesWestToEast.unshift(westIndicesSouthToNorth[westIndicesSouthToNorth.length - 1]);
+        northIndicesWestToEast.push(eastIndicesNorthToSouth[0]);
+
         return {
             vertices : vertices,
             indices : new Uint16Array(indices),
@@ -408,7 +423,11 @@ define([
             orientedBoundingBox : orientedBoundingBox,
             occludeePointInScaledSpace : occludeePointInScaledSpace,
             vertexCountWithoutSkirts : vertexCountWithoutSkirts,
-            skirtIndex : skirtIndex
+            skirtIndex : skirtIndex,
+            westIndicesSouthToNorth : westIndicesSouthToNorth,
+            southIndicesEastToWest : southIndicesEastToWest,
+            eastIndicesNorthToSouth : eastIndicesNorthToSouth,
+            northIndicesWestToEast : northIndicesWestToEast
         };
     }
 
diff --git a/Source/Workers/createVerticesFromHeightmap.js b/Source/Workers/createVerticesFromHeightmap.js
index 21408411117..26f55c5be16 100644
--- a/Source/Workers/createVerticesFromHeightmap.js
+++ b/Source/Workers/createVerticesFromHeightmap.js
@@ -36,7 +36,11 @@ define([
             boundingSphere3D : statistics.boundingSphere3D,
             orientedBoundingBox : statistics.orientedBoundingBox,
             occludeePointInScaledSpace : statistics.occludeePointInScaledSpace,
-            encoding : statistics.encoding
+            encoding : statistics.encoding,
+            westIndicesSouthToNorth : statistics.westIndicesSouthToNorth,
+            southIndicesEastToWest : statistics.southIndicesEastToWest,
+            eastIndicesNorthToSouth : statistics.eastIndicesNorthToSouth,
+            northIndicesWestToEast : statistics.northIndicesWestToEast
         };
     }
 
diff --git a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js
index bbd328bdd0f..8d9f5b0d6f3 100644
--- a/Source/Workers/createVerticesFromQuantizedTerrainMesh.js
+++ b/Source/Workers/createVerticesFromQuantizedTerrainMesh.js
@@ -102,8 +102,11 @@ define([
         var maxLatitude = Number.NEGATIVE_INFINITY;
 
         for (var i = 0; i < quantizedVertexCount; ++i) {
-            var u = uBuffer[i] / maxShort;
-            var v = vBuffer[i] / maxShort;
+            var rawU = uBuffer[i];
+            var rawV = vBuffer[i];
+
+            var u = rawU / maxShort;
+            var v = rawV / maxShort;
             var height = CesiumMath.lerp(minimumHeight, maximumHeight, heightBuffer[i] / maxShort);
 
             cartographicScratch.longitude = CesiumMath.lerp(west, east, u);
@@ -131,6 +134,19 @@ define([
             Cartesian3.maximumByComponent(cartesian3Scratch, maximum, maximum);
         }
 
+        var westIndicesSouthToNorth = copyAndSort(parameters.westIndices, function(a, b) {
+            return uvs[a].y - uvs[b].y;
+        });
+        var eastIndicesNorthToSouth = copyAndSort(parameters.eastIndices, function(a, b) {
+            return uvs[b].y - uvs[a].y;
+        });
+        var southIndicesEastToWest = copyAndSort(parameters.southIndices, function(a, b) {
+            return uvs[b].x - uvs[a].x;
+        });
+        var northIndicesWestToEast = copyAndSort(parameters.northIndices, function(a, b) {
+            return uvs[a].x - uvs[b].x;
+        });
+
         var orientedBoundingBox;
         var boundingSphere;
 
@@ -211,6 +227,10 @@ define([
         return {
             vertices : vertexBuffer.buffer,
             indices : indexBuffer.buffer,
+            westIndicesSouthToNorth : westIndicesSouthToNorth,
+            southIndicesEastToWest : southIndicesEastToWest,
+            eastIndicesNorthToSouth : eastIndicesNorthToSouth,
+            northIndicesWestToEast : northIndicesWestToEast,
             vertexStride : vertexStride,
             center : center,
             minimumHeight : minimumHeight,
@@ -338,5 +358,24 @@ define([
         return indexBufferIndex;
     }
 
+    function copyAndSort(typedArray, comparator) {
+        var copy;
+        if (typeof typedArray.slice === 'function') {
+            copy = typedArray.slice();
+            if (typeof copy.sort !== 'function') {
+                // Sliced typed array isn't sortable, so we can't use it.
+                copy = undefined;
+            }
+        }
+
+        if (!defined(copy)) {
+            copy = Array.prototype.slice.call(typedArray);
+        }
+
+        copy.sort(comparator);
+
+        return copy;
+    }
+
     return createTaskProcessorWorker(createVerticesFromQuantizedTerrainMesh);
 });
diff --git a/Source/Workers/upsampleQuantizedTerrainMesh.js b/Source/Workers/upsampleQuantizedTerrainMesh.js
index 1d4b737e5a8..8143ff428fc 100644
--- a/Source/Workers/upsampleQuantizedTerrainMesh.js
+++ b/Source/Workers/upsampleQuantizedTerrainMesh.js
@@ -96,41 +96,40 @@ define([
         var height;
 
         var i, n;
+        var u, v;
         for (i = 0, n = 0; i < quantizedVertexCount; ++i, n += 2) {
             var texCoords = encoding.decodeTextureCoordinates(parentVertices, i, decodeTexCoordsScratch);
             height  = encoding.decodeHeight(parentVertices, i) / exaggeration;
 
-            parentUBuffer[i] = CesiumMath.clamp((texCoords.x * maxShort) | 0, 0, maxShort);
-            parentVBuffer[i] = CesiumMath.clamp((texCoords.y * maxShort) | 0, 0, maxShort);
+            u = CesiumMath.clamp((texCoords.x * maxShort) | 0, 0, maxShort);
+            v = CesiumMath.clamp((texCoords.y * maxShort) | 0, 0, maxShort);
             parentHeightBuffer[i] = CesiumMath.clamp((((height - parentMinimumHeight) / (parentMaximumHeight - parentMinimumHeight)) * maxShort) | 0, 0, maxShort);
 
-            if (parentUBuffer[i] < threshold) {
-                parentUBuffer[i] = 0;
+            if (u < threshold) {
+                u = 0;
             }
 
-            if (parentVBuffer[i] < threshold) {
-                parentVBuffer[i] = 0;
+            if (v < threshold) {
+                v = 0;
             }
 
-            if (maxShort - parentUBuffer[i] < threshold) {
-                parentUBuffer[i] = maxShort;
+            if (maxShort - u < threshold) {
+                u = maxShort;
             }
 
-            if (maxShort - parentVBuffer[i] < threshold) {
-                parentVBuffer[i] = maxShort;
+            if (maxShort - v < threshold) {
+                v = maxShort;
             }
 
+            parentUBuffer[i] = u;
+            parentVBuffer[i] = v;
+
             if (hasVertexNormals) {
                 var encodedNormal = encoding.getOctEncodedNormal(parentVertices, i, octEncodedNormalScratch);
                 parentNormalBuffer[n] = encodedNormal.x;
                 parentNormalBuffer[n + 1] = encodedNormal.y;
             }
-        }
 
-        var u, v;
-        for (i = 0, n = 0; i < quantizedVertexCount; ++i, n += 2) {
-            u = parentUBuffer[i];
-            v = parentVBuffer[i];
             if ((isEastChild && u >= halfMaxShort || !isEastChild && u <= halfMaxShort) &&
                 (isNorthChild && v >= halfMaxShort || !isNorthChild && v <= halfMaxShort)) {
 
diff --git a/Specs/Core/MathSpec.js b/Specs/Core/MathSpec.js
index 60876d81eef..fba21c6269d 100644
--- a/Specs/Core/MathSpec.js
+++ b/Specs/Core/MathSpec.js
@@ -254,6 +254,110 @@ defineSuite([
         }).toThrowDeveloperError();
     });
 
+    it('leftIsLessThanRight works', function() {
+        expect(CesiumMath.leftIsLessThanRight(1.0, 2.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsLessThanRight(2.0, 1.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsLessThanRight(1.0, 1.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsLessThanRight(1.0, 1.2, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsLessThanRight(1.2, 1.0, 0.2)).toBe(false);
+    });
+
+    it('leftIsLessThanRight throws for undefined left', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanRight(undefined, 5.0, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsLessThanRight throws for undefined right', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanRight(1.0, undefined, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsLessThanRight throws for undefined absoluteEpsilon', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanRight(1.0, 5.0, undefined);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsLessThanOrEqualToRight works', function() {
+        expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 2.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsLessThanOrEqualToRight(2.0, 1.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 1.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsLessThanOrEqualToRight(1.0, 1.2, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsLessThanOrEqualToRight(1.2, 1.0, 0.2)).toBe(true);
+    });
+
+    it('leftIsLessThanOrEqualToRight throws for undefined left', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanOrEqualToRight(undefined, 5.0, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsLessThanOrEqualToRight throws for undefined right', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanOrEqualToRight(1.0, undefined, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsLessThanOrEqualToRight throws for undefined absoluteEpsilon', function() {
+        expect(function() {
+            CesiumMath.leftIsLessThanOrEqualToRight(1.0, 5.0, undefined);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanRight works', function() {
+        expect(CesiumMath.leftIsGreaterThanRight(1.0, 2.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsGreaterThanRight(2.0, 1.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsGreaterThanRight(1.0, 1.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsGreaterThanRight(1.0, 1.2, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsGreaterThanRight(1.2, 1.0, 0.2)).toBe(false);
+    });
+
+    it('leftIsGreaterThanRight throws for undefined left', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanRight(undefined, 5.0, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanRight throws for undefined right', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanRight(1.0, undefined, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanRight throws for undefined absoluteEpsilon', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanRight(1.0, 5.0, undefined);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanOrEqualToRight works', function() {
+        expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 2.0, 0.2)).toBe(false);
+        expect(CesiumMath.leftIsGreaterThanOrEqualToRight(2.0, 1.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 1.0, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 1.2, 0.2)).toBe(true);
+        expect(CesiumMath.leftIsGreaterThanOrEqualToRight(1.2, 1.0, 0.2)).toBe(true);
+    });
+
+    it('leftIsGreaterThanOrEqualToRight throws for undefined left', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanOrEqualToRight(undefined, 5.0, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanOrEqualToRight throws for undefined right', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, undefined, CesiumMath.EPSILON16);
+        }).toThrowDeveloperError();
+    });
+
+    it('leftIsGreaterThanOrEqualToRight throws for undefined absoluteEpsilon', function() {
+        expect(function() {
+            CesiumMath.leftIsGreaterThanOrEqualToRight(1.0, 5.0, undefined);
+        }).toThrowDeveloperError();
+    });
+
     it('factorial produces the correct results', function() {
         var factorials = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600, 6227020800, 87178291200, 1307674368000, 20922789888000, 355687428096000, 6402373705728000,
                           121645100408832000, 2432902008176640000, 51090942171709440000, 1124000727777607680000, 25852016738884976640000, 620448401733239439360000];
diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js
index 55f49fc1492..d01723d7e39 100644
--- a/Specs/Core/RequestSchedulerSpec.js
+++ b/Specs/Core/RequestSchedulerSpec.js
@@ -610,6 +610,7 @@ defineSuite([
             return new Request({
                 url : 'http://test.invalid/1',
                 requestFunction : requestFunction,
+                throttle : throttleByServer,
                 throttleByServer : throttleByServer
             });
         }
diff --git a/Specs/Core/TerrainEncodingSpec.js b/Specs/Core/TerrainEncodingSpec.js
index 34dfc5a5e26..33af2e708fb 100644
--- a/Specs/Core/TerrainEncodingSpec.js
+++ b/Specs/Core/TerrainEncodingSpec.js
@@ -38,7 +38,7 @@ defineSuite([
 
     it('default constructs', function() {
         var encoding = new TerrainEncoding();
-        expect(encoding.quantization).not.toBeDefined();
+        expect(encoding.quantization).toBe(TerrainQuantization.NONE);
         expect(encoding.minimumHeight).not.toBeDefined();
         expect(encoding.maximumHeight).not.toBeDefined();
         expect(encoding.center).not.toBeDefined();
diff --git a/Specs/DataSources/EntityClusterSpec.js b/Specs/DataSources/EntityClusterSpec.js
index 0770d508f5a..ef98d250f96 100644
--- a/Specs/DataSources/EntityClusterSpec.js
+++ b/Specs/DataSources/EntityClusterSpec.js
@@ -50,7 +50,6 @@ defineSuite([
                     tilesWaitingForChildren : 0
                 }
             },
-            tileLoadedEvent : new Event(),
             terrainProviderChanged : new Event(),
             imageryLayersUpdatedEvent : new Event(),
             beginFrame : function() {},
diff --git a/Specs/DataSources/PointVisualizerSpec.js b/Specs/DataSources/PointVisualizerSpec.js
index 07853ca88d2..de57e0ecbbf 100644
--- a/Specs/DataSources/PointVisualizerSpec.js
+++ b/Specs/DataSources/PointVisualizerSpec.js
@@ -51,7 +51,6 @@ defineSuite([
         scene.globe = {
             ellipsoid : Ellipsoid.WGS84,
             _surface : {},
-            tileLoadedEvent : new Event(),
             imageryLayersUpdatedEvent : new Event(),
             terrainProviderChanged : new Event()
         };
diff --git a/Specs/MockImageryProvider.js b/Specs/MockImageryProvider.js
new file mode 100644
index 00000000000..6afa3e142aa
--- /dev/null
+++ b/Specs/MockImageryProvider.js
@@ -0,0 +1,73 @@
+define([
+    'Core/GeographicTilingScheme',
+    'Core/Resource',
+    'Core/RuntimeError',
+    'ThirdParty/when',
+    './createTileKey',
+    './runLater'
+], function(
+    GeographicTilingScheme,
+    Resource,
+    RuntimeError,
+    when,
+    createTileKey,
+    runLater) {
+   'use strict';
+
+    function MockImageryProvider() {
+        this.tilingScheme = new GeographicTilingScheme();
+        this.ready = false;
+        this.rectangle = this.tilingScheme.rectangle;
+        this.tileWidth = 256;
+        this.tileHeight = 256;
+        this._requestImageWillSucceed = {};
+
+        var that = this;
+        Resource.fetchImage('./Data/Images/Green.png').then(function(image) {
+            that.ready = true;
+            that._image = image;
+        });
+    }
+
+    MockImageryProvider.prototype.requestImage = function(x, y, level, request) {
+        var willSucceed = this._requestImageWillSucceed[createTileKey(x, y, level)];
+        if (willSucceed === undefined) {
+            return undefined; // defer by default
+        }
+
+        var that = this;
+        return runLater(function() {
+            if (willSucceed === true) {
+                return that._image;
+            } else if (willSucceed === false) {
+                throw new RuntimeError('requestImage failed as request.');
+            }
+
+            return when(willSucceed).then(function() {
+                return that._image;
+            });
+        });
+    };
+
+    MockImageryProvider.prototype.requestImageWillSucceed = function(xOrTile, y, level) {
+        this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = true;
+        return this;
+    };
+
+    MockImageryProvider.prototype.requestImageWillFail = function(xOrTile, y, level) {
+        this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = false;
+        return this;
+    };
+
+    MockImageryProvider.prototype.requestImageWillDefer = function(xOrTile, y, level) {
+        this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = undefined;
+        return this;
+    };
+
+    MockImageryProvider.prototype.requestImageWillWaitOn = function(promise, xOrTile, y, level) {
+        this._requestImageWillSucceed[createTileKey(xOrTile, y, level)] = promise;
+        return this;
+    };
+
+    return MockImageryProvider;
+});
diff --git a/Specs/MockTerrainProvider.js b/Specs/MockTerrainProvider.js
new file mode 100644
index 00000000000..beabf712554
--- /dev/null
+++ b/Specs/MockTerrainProvider.js
@@ -0,0 +1,223 @@
+define([
+    'Core/defined',
+    'Core/GeographicTilingScheme',
+    'Core/HeightmapTerrainData',
+    'Core/RuntimeError',
+    'Core/TerrainProvider',
+    'ThirdParty/when',
+    './createTileKey',
+    './runLater'
+], function(
+    defined,
+    GeographicTilingScheme,
+    HeightmapTerrainData,
+    RuntimeError,
+    TerrainProvider,
+    when,
+    createTileKey,
+    runLater) {
+    'use strict';
+
+    function MockTerrainProvider() {
+        this.tilingScheme = new GeographicTilingScheme();
+        this.heightmapWidth = 65;
+        this.levelZeroMaximumGeometricError = TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(this.tilingScheme.ellipsoid, this.heightmapWidth, this.tilingScheme.getNumberOfXTilesAtLevel(0));
+        this.ready = true;
+        this.hasWaterMask = true;
+
+        this._tileDataAvailable = {};
+        this._requestTileGeometryWillSucceed = {};
+        this._requestTileGeometryWillSucceedWith = {};
+        this._willHaveWaterMask = {};
+        this._createMeshWillSucceed = {};
+        this._upsampleWillSucceed = {};
+    }
+
+    MockTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) {
+        var willSucceed = this._requestTileGeometryWillSucceed[createTileKey(x, y, level)];
+        if (willSucceed === undefined) {
+            return undefined; // defer by default
+        }
+
+        var that = this;
+        return runLater(function() {
+            if (willSucceed === true) {
+                return createTerrainData(that, x, y, level, false);
+            } else if (willSucceed === false) {
+                throw new RuntimeError('requestTileGeometry failed as requested.');
+            }
+
+            return when(willSucceed).then(function() {
+                return createTerrainData(that, x, y, level, false);
+            });
+        });
+    };
+
+    MockTerrainProvider.prototype.getTileDataAvailable = function(xOrTile, y, level) {
+        return this._tileDataAvailable[createTileKey(xOrTile, y, level)];
+    };
+
+    MockTerrainProvider.prototype.getLevelMaximumGeometricError = function(level) {
+        return this.levelZeroMaximumGeometricError / (1 << level);
+    };
+
+    MockTerrainProvider.prototype.requestTileGeometryWillSucceed = function(xOrTile, y, level) {
+        this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = true;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.requestTileGeometryWillSucceedWith = function(terrainData, xOrTile, y, level) {
+        this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = true;
+        this._requestTileGeometryWillSucceedWith[createTileKey(xOrTile, y, level)] = terrainData;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.requestTileGeometryWillFail = function(xOrTile, y, level) {
+        this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = false;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.requestTileGeometryWillDefer = function(xOrTile, y, level) {
+        this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = undefined;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.requestTileGeometryWillWaitOn = function(promise, xOrTile, y, level) {
+        this._requestTileGeometryWillSucceed[createTileKey(xOrTile, y, level)] = promise;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.willHaveWaterMask = function(includeLand, includeWater, xOrTile, y, level) {
+        this._willHaveWaterMask[createTileKey(xOrTile, y, level)] = includeLand || includeWater ? {
+            includeLand: includeLand,
+            includeWater: includeWater
+        } : undefined;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.createMeshWillSucceed = function(xOrTile, y, level) {
+        this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = true;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.createMeshWillFail = function(xOrTile, y, level) {
+        this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = false;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.createMeshWillDefer = function(xOrTile, y, level) {
+        this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = undefined;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.createMeshWillWaitOn = function(promise, xOrTile, y, level) {
+        this._createMeshWillSucceed[createTileKey(xOrTile, y, level)] = promise;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.upsampleWillSucceed = function(xOrTile, y, level) {
+        this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = true;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.upsampleWillFail = function(xOrTile, y, level) {
+        this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = false;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.upsampleWillDefer = function(xOrTile, y, level) {
+        this._upsampleWillSucceed[createTileKey(xOrTile, y, level)] = undefined;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.willBeAvailable = function(xOrTile, y, level) {
+        this._tileDataAvailable[createTileKey(xOrTile, y, level)] = true;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.willBeUnavailable = function(xOrTile, y, level) {
+        this._tileDataAvailable[createTileKey(xOrTile, y, level)] = false;
+        return this;
+    };
+
+    MockTerrainProvider.prototype.willBeUnknownAvailability = function(xOrTile, y, level) {
+        this._tileDataAvailable[createTileKey(xOrTile, y, level)] = undefined;
+        return this;
+    };
+
+    function createTerrainData(terrainProvider, x, y, level, upsampled) {
+        var terrainData = terrainProvider._requestTileGeometryWillSucceedWith[createTileKey(x, y, level)];
+
+        if (!defined(terrainData)) {
+            var options = {
+                width: 5,
+                height: 5,
+                buffer: new Float32Array(25),
+                createdByUpsampling: upsampled
+            };
+
+            var willHaveWaterMask = terrainProvider._willHaveWaterMask[createTileKey(x, y, level)];
+            if (defined(willHaveWaterMask)) {
+                if (willHaveWaterMask.includeLand && willHaveWaterMask.includeWater) {
+                    options.waterMask = new Uint8Array(4);
+                    options.waterMask[0] = 1;
+                    options.waterMask[1] = 1;
+                    options.waterMask[2] = 0;
+                    options.waterMask[3] = 0;
+                } else if (willHaveWaterMask.includeLand) {
+                    options.waterMask = new Uint8Array(1);
+                    options.waterMask[0] = 0;
+                } else if (willHaveWaterMask.includeWater) {
+                    options.waterMask = new Uint8Array(1);
+                    options.waterMask[0] = 1;
+                }
+            }
+
+            terrainData = new HeightmapTerrainData(options);
+        }
+
+        var originalUpsample = terrainData.upsample;
+        terrainData.upsample = function(tilingScheme, thisX, thisY, thisLevel, descendantX, descendantY) {
+            var willSucceed = terrainProvider._upsampleWillSucceed[createTileKey(descendantX, descendantY, thisLevel + 1)];
+            if (willSucceed === undefined) {
+                return undefined; // defer by default
+            }
+
+            if (willSucceed) {
+                return originalUpsample.apply(terrainData, arguments);
+            }
+
+            return runLater(function() {
+                throw new RuntimeError('upsample failed as requested.');
+            });
+        };
+
+        var originalCreateMesh = terrainData.createMesh;
+        terrainData.createMesh = function(tilingScheme, x, y, level) {
+            var willSucceed = terrainProvider._createMeshWillSucceed[createTileKey(x, y, level)];
+            if (willSucceed === undefined) {
+                return undefined; // defer by default
+            }
+
+            if (willSucceed === true) {
+                return originalCreateMesh.apply(terrainData, arguments);
+            } else if (willSucceed === false) {
+                return runLater(function() {
+                    throw new RuntimeError('createMesh failed as requested.');
+                });
+            }
+
+            var args = arguments;
+
+            return runLater(function() {
+                return when(willSucceed).then(function() {
+                    return originalCreateMesh.apply(terrainData, args);
+                });
+            });
+        };
+
+        return terrainData;
+    }
+
+    return MockTerrainProvider;
+});
diff --git a/Specs/Scene/GlobeSurfaceTileProviderSpec.js b/Specs/Scene/GlobeSurfaceTileProviderSpec.js
index efbb23c3229..4220a581668 100644
--- a/Specs/Scene/GlobeSurfaceTileProviderSpec.js
+++ b/Specs/Scene/GlobeSurfaceTileProviderSpec.js
@@ -304,7 +304,7 @@ defineSuite([
                 // Verify that each tile has 2 imagery objects and no loaded callbacks
                 forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) {
                     expect(tile.data.imagery.length).toBe(2);
-                    expect(Object.keys(tile._loadedCallbacks).length).toBe(1);
+                    expect(Object.keys(tile._loadedCallbacks).length).toBe(0);
                 });
 
                 // Reload each layer
@@ -319,14 +319,14 @@ defineSuite([
                 //  and also has 2 callbacks so the old imagery will be removed once loaded.
                 forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) {
                     expect(tile.data.imagery.length).toBe(4);
-                    expect(Object.keys(tile._loadedCallbacks).length).toBe(3);
+                    expect(Object.keys(tile._loadedCallbacks).length).toBe(2);
                 });
 
                 return updateUntilDone(scene.globe).then(function() {
                     // Verify the old imagery was removed and the callbacks are no longer there
                     forEachRenderedTile(scene.globe._surface, 1, undefined, function(tile) {
                         expect(tile.data.imagery.length).toBe(2);
-                        expect(Object.keys(tile._loadedCallbacks).length).toBe(1);
+                        expect(Object.keys(tile._loadedCallbacks).length).toBe(0);
                     });
                 });
             });
diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js
index e3bcb4764fb..b4bb3e7877c 100644
--- a/Specs/Scene/GlobeSurfaceTileSpec.js
+++ b/Specs/Scene/GlobeSurfaceTileSpec.js
@@ -2,118 +2,78 @@ defineSuite([
         'Scene/GlobeSurfaceTile',
         'Core/Cartesian3',
         'Core/Cartesian4',
-        'Core/CesiumTerrainProvider',
+        'Core/clone',
         'Core/createWorldTerrain',
-        'Core/defined',
         'Core/Ellipsoid',
         'Core/GeographicTilingScheme',
         'Core/Ray',
-        'Core/Rectangle',
-        'Core/RequestScheduler',
-        'Scene/Imagery',
+        'Renderer/Texture',
         'Scene/ImageryLayer',
         'Scene/ImageryLayerCollection',
-        'Scene/ImageryState',
         'Scene/QuadtreeTile',
         'Scene/QuadtreeTileLoadState',
         'Scene/TerrainState',
-        'Scene/TileImagery',
         'Specs/createScene',
-        'Specs/pollToPromise',
-        'ThirdParty/when'
+        'ThirdParty/when',
+        '../MockImageryProvider',
+        '../MockTerrainProvider',
+        '../TerrainTileProcessor'
     ], function(
         GlobeSurfaceTile,
         Cartesian3,
         Cartesian4,
-        CesiumTerrainProvider,
+        clone,
         createWorldTerrain,
-        defined,
         Ellipsoid,
         GeographicTilingScheme,
         Ray,
-        Rectangle,
-        RequestScheduler,
-        Imagery,
+        Texture,
         ImageryLayer,
         ImageryLayerCollection,
-        ImageryState,
         QuadtreeTile,
         QuadtreeTileLoadState,
         TerrainState,
-        TileImagery,
         createScene,
-        pollToPromise,
-        when) {
+        when,
+        MockImageryProvider,
+        MockTerrainProvider,
+        TerrainTileProcessor) {
     'use strict';
 
-    describe('processStateMachine', function() {
-        var scene;
-        var alwaysDeferTerrainProvider;
-        var alwaysFailTerrainProvider;
-        var realTerrainProvider;
+    var frameState;
+    var tilingScheme;
+    var rootTiles;
+    var rootTile;
+    var imageryLayerCollection;
+    var mockTerrain;
+    var processor;
+
+    beforeEach(function() {
+        frameState = {
+            context: {
+                cache: {}
+            }
+        };
 
-        var tilingScheme;
-        var rootTiles;
-        var rootTile;
-        var imageryLayerCollection;
+        tilingScheme = new GeographicTilingScheme();
+        rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+        rootTile = rootTiles[0];
+        imageryLayerCollection = new ImageryLayerCollection();
 
-        beforeAll(function() {
-            scene = createScene();
+        mockTerrain = new MockTerrainProvider();
 
-            alwaysDeferTerrainProvider = {
-                requestTileGeometry : function(x, y, level) {
-                    return undefined;
-                },
-                tilingScheme : tilingScheme,
-                hasWaterMask : function() {
-                    return true;
-                },
-                getTileDataAvailable : function(x, y, level) {
-                    return undefined;
-                }
-            };
-
-            alwaysFailTerrainProvider = {
-                requestTileGeometry : function(x, y, level) {
-                    var deferred = when.defer();
-                    deferred.reject();
-                    return deferred.promise;
-                },
-                tilingScheme : tilingScheme,
-                hasWaterMask : function() {
-                    return true;
-                },
-                getTileDataAvailable : function(x, y, level) {
-                    return undefined;
-                }
-            };
-
-            realTerrainProvider = new CesiumTerrainProvider({
-                url : 'https://s3.amazonaws.com/cesiumjs/smallTerrain'
-            });
-        });
+        processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection);
+    });
 
-        afterAll(function() {
-            scene.destroyForSpecs();
-        });
+    afterEach(function() {
+        for (var i = 0; i < rootTiles.length; ++i) {
+            rootTiles[i].freeResources();
+        }
+    });
 
+    describe('processStateMachine', function() {
         beforeEach(function() {
-            tilingScheme = new GeographicTilingScheme();
-            alwaysDeferTerrainProvider.tilingScheme = tilingScheme;
-            alwaysFailTerrainProvider.tilingScheme = tilingScheme;
-            rootTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
-            rootTile = rootTiles[0];
-            imageryLayerCollection = new ImageryLayerCollection();
-
-            return pollToPromise(function() {
-                return realTerrainProvider.ready;
-            });
-        });
-
-        afterEach(function() {
-            for (var i = 0; i < rootTiles.length; ++i) {
-                rootTiles[i].freeResources();
-            }
+            processor.mockWebGL();
         });
 
         it('starts in the START state', function() {
@@ -123,401 +83,217 @@ defineSuite([
             }
         });
 
-        it('transitions to the LOADING state immediately', function() {
-            GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-            expect(rootTile.state).toBe(QuadtreeTileLoadState.LOADING);
-        });
+        it('transitions to the LOADING state immediately if this tile is available', function() {
+            mockTerrain
+                .willBeAvailable(rootTile.southwestChild);
 
-        it('creates loadedTerrain but not upsampledTerrain for root tiles', function() {
-            GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-            expect(rootTile.data.loadedTerrain).toBeDefined();
-            expect(rootTile.data.upsampledTerrain).toBeUndefined();
+            return processor.process([rootTile.southwestChild]).then(function() {
+                expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING);
+                expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.UNLOADED);
+            });
         });
 
-        it('non-root tiles get neither loadedTerrain nor upsampledTerrain when their parent is not loaded nor upsampled', function() {
-            var children = rootTile.children;
-            for (var i = 0; i < children.length; ++i) {
-                GlobeSurfaceTile.processStateMachine(children[i], scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                expect(children[i].data.loadedTerrain).toBeUndefined();
-                expect(children[i].data.upsampledTerrain).toBeUndefined();
-            }
-        });
+        it('transitions to the LOADING tile state and FAILED terrain state immediately if this tile is NOT available', function() {
+            mockTerrain
+                .willBeUnavailable(rootTile.southwestChild);
 
-        it('once a root tile is loaded, its children get both loadedTerrain and upsampledTerrain', function() {
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return rootTile.state === QuadtreeTileLoadState.DONE;
-            }).then(function() {
-                var children = rootTile.children;
-                for (var i = 0; i < children.length; ++i) {
-                    GlobeSurfaceTile.processStateMachine(children[i], scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    expect(children[i].data.loadedTerrain).toBeDefined();
-                    expect(children[i].data.upsampledTerrain).toBeDefined();
-                }
+            return processor.process([rootTile.southwestChild]).then(function() {
+                expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.LOADING);
+                expect(rootTile.southwestChild.data.terrainState).toBe(TerrainState.FAILED);
             });
         });
 
-        it('loaded terrainData is copied to the tile once it is available', function() {
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-            }).then(function() {
-                expect(rootTile.data.terrainData).toBeDefined();
-            });
-        });
+        it('pushes parent along if waiting on it to be able to upsample', function() {
+            mockTerrain
+                .willBeAvailable(rootTile)
+                .requestTileGeometryWillSucceed(rootTile)
+                .willBeUnavailable(rootTile.southwestChild);
 
-        xit('upsampled terrainData is copied to the tile once it is available', function() {
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-            }).then(function() {
-                return pollToPromise(function() {
-                    var childTile = rootTile.children[0];
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                }).then(function() {
-                    expect(rootTile.children[0].data.terrainData).toBeDefined();
-                });
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return processor.process([rootTile.southwestChild]).then(function() {
+                expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(0);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(0);
             });
         });
 
-        xit('loaded terrain data replaces upsampled terrain data', function() {
-            var childTile = rootTile.children[0];
+        it('does nothing when a root tile is unavailable', function() {
+            mockTerrain
+                .willBeUnavailable(rootTile);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-            }).then(function() {
-                var upsampledTerrainData;
-
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                }).then(function() {
-                    upsampledTerrainData = childTile.data.terrainData;
-                    expect(upsampledTerrainData).toBeDefined();
-
-                    return pollToPromise(function() {
-                        GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                        return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-                    }).then(function() {
-                        expect(childTile.data.terrainData).not.toBe(upsampledTerrainData);
-                    });
-                });
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED);
+                expect(rootTile.data.terrainState).toBe(TerrainState.FAILED);
             });
         });
 
-        xit('loaded terrain replacing upsampled terrain triggers re-upsampling and re-loading of children', function() {
-            var childTile = rootTile.children[0];
-            var grandchildTile = childTile.children[0];
+        it('does nothing when a root tile fails to load', function() {
+            mockTerrain
+                .requestTileGeometryWillFail(rootTile);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-            }).then(function() {
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                });
-            }).then(function() {
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                });
-            }).then(function() {
-                var grandchildUpsampledTerrain = grandchildTile.data.upsampledTerrain;
-                expect(grandchildTile.data.loadedTerrain).toBeUndefined();
-
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                    return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-                }).then(function() {
-                    expect(grandchildTile.data.upsampledTerrain).not.toBe(grandchildUpsampledTerrain);
-                    expect(grandchildTile.data.loadedTerrain).toBeDefined();
-                });
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.state).toBe(QuadtreeTileLoadState.FAILED);
+                expect(rootTile.data.terrainState).toBe(TerrainState.FAILED);
             });
         });
 
-        xit('improved upsampled terrain triggers re-upsampling of children', function() {
-            var childTile = rootTile.children[0];
-            var grandchildTile = childTile.children[0];
-            var greatGrandchildTile = grandchildTile.children[0];
+        it('upsamples failed tiles from parent TerrainData', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .createMeshWillSucceed(rootTile)
+                .willBeUnavailable(rootTile.southwestChild)
+                .upsampleWillSucceed(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-            }).then(function() {
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return childTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                });
-            }).then(function() {
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                });
-            }).then(function() {
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(greatGrandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return greatGrandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                });
-            }).then(function() {
-               return pollToPromise(function() {
-                   GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                   return childTile.data.loadedTerrain.state >= TerrainState.RECEIVED;
-               });
-            }).then(function() {
-                var greatGrandchildUpsampledTerrain = grandchildTile.data.upsampledTerrain;
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                    return grandchildTile.data.upsampledTerrain.state >= TerrainState.RECEIVED;
-                }).then(function() {
-                    expect(greatGrandchildTile.data.upsampledTerrain).toBeDefined();
-                    expect(greatGrandchildTile.data.upsampledTerrain).not.toBe(greatGrandchildUpsampledTerrain);
-                });
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
+                expect(rootTile.data.terrainData.wasCreatedByUpsampling()).toBe(false);
+                expect(rootTile.southwestChild.data.terrainData.wasCreatedByUpsampling()).toBe(true);
             });
         });
 
-        it('releases previous upsampled water mask when a real one is loaded', function() {
-            var childTile = rootTile.children[0];
+        it('loads available tiles', function() {
+            mockTerrain
+                .willBeAvailable(rootTile.southwestChild)
+                .requestTileGeometryWillSucceed(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return rootTile.renderable && childTile.renderable;
-            }).then(function() {
-                expect(childTile.data.waterMaskTexture).toBeDefined();
-                var childWaterMaskTexture = childTile.data.waterMaskTexture;
-                var referenceCount = childWaterMaskTexture.referenceCount;
-                var vertexArraysToDestroy = [];
-
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy);
-                    RequestScheduler.update();
-                    return childTile.state === QuadtreeTileLoadState.DONE;
-                }).then(function() {
-                    expect(childTile.data.waterMaskTexture).toBeDefined();
-                    expect(childTile.data.waterMaskTexture).not.toBe(childWaterMaskTexture);
-                    expect(childWaterMaskTexture.referenceCount + 1).toBe(referenceCount);
-                    expect(vertexArraysToDestroy.length).toEqual(1);
-                });
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return processor.process([rootTile.southwestChild]).then(function() {
+                expect(mockTerrain.requestTileGeometry.calls.count()).toBe(1);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[0]).toBe(0);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[1]).toBe(1);
+                expect(mockTerrain.requestTileGeometry.calls.argsFor(0)[2]).toBe(1);
             });
         });
 
-        it('upsampled terrain is used when real terrain fails to load', function() {
-            var childTile = rootTile.children[0];
+        it('marks an upsampled tile as such', function() {
+            mockTerrain
+                .willBeAvailable(rootTile)
+                .requestTileGeometryWillSucceed(rootTile)
+                .createMeshWillSucceed(rootTile)
+                .willBeUnavailable(rootTile.southwestChild)
+                .upsampleWillSucceed(rootTile.southwestChild)
+                .createMeshWillSucceed(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return rootTile.renderable && childTile.renderable;
-            }).then(function() {
-                expect(childTile.data.loadedTerrain).toBeUndefined();
-                expect(childTile.upsampledFromParent).toBe(true);
-            });
-        });
+            var mockImagery = new MockImageryProvider();
+            imageryLayerCollection.addImageryProvider(mockImagery);
 
-        it('child of loaded tile is not re-upsampled or re-loaded if it is already loaded', function() {
-            var childTile = rootTile.children[0];
-            var grandchildTile = childTile.children[0];
+            mockImagery
+                .requestImageWillSucceed(rootTile)
+                .requestImageWillFail(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) &&
-                       defined(childTile.data.terrainData);
-            }).then(function() {
-                // Mark the grandchild as present even though the child is upsampled.
-                childTile.data.terrainData._childTileMask = 15;
-
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                    RequestScheduler.update();
-                    return grandchildTile.state === QuadtreeTileLoadState.DONE;
-                }).then(function() {
-                    expect(grandchildTile.data.loadedTerrain).toBeUndefined();
-                    expect(grandchildTile.data.upsampledTerrain).toBeUndefined();
-
-                    var vertexArraysToDestroy = [];
-
-                    return pollToPromise(function() {
-                        GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy);
-                        RequestScheduler.update();
-                        return childTile.state === QuadtreeTileLoadState.DONE;
-                    }).then(function() {
-                        expect(grandchildTile.state).toBe(QuadtreeTileLoadState.DONE);
-                        expect(grandchildTile.data.loadedTerrain).toBeUndefined();
-                        expect(grandchildTile.data.upsampledTerrain).toBeUndefined();
-                        expect(vertexArraysToDestroy.length).toEqual(1);
-                    });
-                });
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
+                expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE);
+                expect(rootTile.upsampledFromParent).toBe(false);
+                expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE);
+                expect(rootTile.southwestChild.upsampledFromParent).toBe(true);
             });
         });
 
-        it('child of upsampled tile is not re-upsampled if it is already loaded', function() {
-            var childTile = rootTile.children[0];
-            var grandchildTile = childTile.children[0];
-            var greatGrandchildTile = grandchildTile.children[0];
-
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) &&
-                       defined(childTile.data.terrainData) && defined(childTile.data.terrainData._mesh) &&
-                       defined(grandchildTile.data.terrainData);
-            }).then(function() {
-                // Mark the great-grandchild as present even though the grandchild is upsampled.
-                grandchildTile.data.terrainData._childTileMask = 15;
-
-                return pollToPromise(function() {
-                    GlobeSurfaceTile.processStateMachine(greatGrandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                    RequestScheduler.update();
-                    return greatGrandchildTile.state === QuadtreeTileLoadState.DONE;
-                }).then(function() {
-                    expect(greatGrandchildTile.data.loadedTerrain).toBeUndefined();
-                    expect(greatGrandchildTile.data.upsampledTerrain).toBeUndefined();
-
-                    var vertexArraysToBeDestroyed = [];
-
-                    return pollToPromise(function() {
-                        GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed);
-                        GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed);
-                        RequestScheduler.update();
-                        return childTile.state === QuadtreeTileLoadState.DONE &&
-                               !defined(grandchildTile.data.upsampledTerrain);
-                    }).then(function() {
-                        expect(greatGrandchildTile.state).toBe(QuadtreeTileLoadState.DONE);
-                        expect(greatGrandchildTile.data.loadedTerrain).toBeUndefined();
-                        expect(greatGrandchildTile.data.upsampledTerrain).toBeUndefined();
-                        expect(vertexArraysToBeDestroyed.length).toEqual(2);
-                    });
-                });
+        it('does not mark a tile as upsampled if it has fresh imagery', function() {
+            mockTerrain
+                .willBeAvailable(rootTile)
+                .requestTileGeometryWillSucceed(rootTile)
+                .createMeshWillSucceed(rootTile)
+                .willBeUnavailable(rootTile.southwestChild)
+                .upsampleWillSucceed(rootTile.southwestChild)
+                .createMeshWillSucceed(rootTile.southwestChild);
+
+            var mockImagery = new MockImageryProvider();
+            imageryLayerCollection.addImageryProvider(mockImagery);
+
+            mockImagery
+                .requestImageWillSucceed(rootTile)
+                .requestImageWillSucceed(rootTile.southwestChild);
+
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
+                expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE);
+                expect(rootTile.upsampledFromParent).toBe(false);
+                expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE);
+                expect(rootTile.southwestChild.upsampledFromParent).toBe(false);
             });
         });
 
-        it('entirely upsampled tile is marked as such', function() {
-            var childTile = rootTile.children[0];
+        it('does not mark a tile as upsampled if it has fresh terrain', function() {
+            mockTerrain
+                .willBeAvailable(rootTile)
+                .requestTileGeometryWillSucceed(rootTile)
+                .createMeshWillSucceed(rootTile)
+                .willBeAvailable(rootTile.southwestChild)
+                .requestTileGeometryWillSucceed(rootTile.southwestChild)
+                .createMeshWillSucceed(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []);
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return rootTile.state >= QuadtreeTileLoadState.DONE &&
-                       childTile.state >= QuadtreeTileLoadState.DONE;
-            }).then(function() {
+            var mockImagery = new MockImageryProvider();
+            imageryLayerCollection.addImageryProvider(mockImagery);
+
+            mockImagery
+                .requestImageWillSucceed(rootTile)
+                .requestImageWillFail(rootTile.southwestChild);
+
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
                 expect(rootTile.state).toBe(QuadtreeTileLoadState.DONE);
-                expect(childTile.upsampledFromParent).toBe(true);
+                expect(rootTile.upsampledFromParent).toBe(false);
+                expect(rootTile.southwestChild.state).toBe(QuadtreeTileLoadState.DONE);
+                expect(rootTile.southwestChild.upsampledFromParent).toBe(false);
             });
         });
 
-        it('uses shared water mask texture for tiles that are entirely water', function() {
-            var allWaterTerrainProvider = {
-                requestTileGeometry : function(x, y, level) {
-                    var real = realTerrainProvider.requestTileGeometry(x, y, level);
-                    if (!defined(real)) {
-                        return real;
-                    }
-
-                    return when(real, function(terrainData) {
-                        terrainData._waterMask = new Uint8Array([255]);
-                        return terrainData;
-                    });
-                },
-                tilingScheme : realTerrainProvider.tilingScheme,
-                hasWaterMask : function() {
-                    return realTerrainProvider.hasWaterMask();
-                },
-                getTileDataAvailable : function(x, y, level) {
-                    return undefined;
-                }
-            };
-
-            var childTile = rootTile.children[0];
-
-            return pollToPromise(function() {
-                if (rootTile.state !== QuadtreeTileLoadState.DONE) {
-                    GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, allWaterTerrainProvider, imageryLayerCollection, []);
-                    return false;
-                }
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, allWaterTerrainProvider, imageryLayerCollection, []);
-                return childTile.state === QuadtreeTileLoadState.DONE;
-            }).then(function() {
-                expect(childTile.data.waterMaskTexture).toBeDefined();
-                expect(childTile.data.waterMaskTexture).toBe(rootTile.data.waterMaskTexture);
+        it('creates water mask texture from one-byte water mask data, if it exists', function() {
+            mockTerrain
+                .willBeAvailable(rootTile)
+                .requestTileGeometryWillSucceed(rootTile)
+                .willHaveWaterMask(false, true, rootTile);
+
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.data.waterMaskTexture).toBeDefined();
             });
         });
 
         it('uses undefined water mask texture for tiles that are entirely land', function() {
-            var allLandTerrainProvider = {
-                requestTileGeometry : function(x, y, level) {
-                    var real = realTerrainProvider.requestTileGeometry(x, y, level);
-                    if (!defined(real)) {
-                        return real;
-                    }
-
-                    return when(real, function(terrainData) {
-                        terrainData._waterMask = new Uint8Array([0]);
-                        return terrainData;
-                    });
-                },
-                tilingScheme : realTerrainProvider.tilingScheme,
-                hasWaterMask : function() {
-                    return realTerrainProvider.hasWaterMask();
-                },
-                getTileDataAvailable : function(x, y, level) {
-                    return undefined;
-                }
-            };
-
-            var childTile = rootTile.children[0];
-
-            return pollToPromise(function() {
-                if (rootTile.state !== QuadtreeTileLoadState.DONE) {
-                    GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, allLandTerrainProvider, imageryLayerCollection, []);
-                    return false;
-                }
-                GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, allLandTerrainProvider, imageryLayerCollection, []);
-                return childTile.state === QuadtreeTileLoadState.DONE;
-            }).then(function() {
-                expect(childTile.data.waterMaskTexture).toBeUndefined();
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .willHaveWaterMask(true, false, rootTile);
+
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.data.waterMaskTexture).toBeUndefined();
             });
         });
 
-        it('loads parent imagery tile even for root terrain tiles', function() {
-            var tile = new QuadtreeTile({
-                tilingScheme : new GeographicTilingScheme(),
-                level : 0,
-                x : 1,
-                y : 0
+        it('uses shared water mask texture for tiles that are entirely water', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .willHaveWaterMask(false, true, rootTile)
+                .requestTileGeometryWillSucceed(rootTile.southwestChild)
+                .willHaveWaterMask(false, true, rootTile.southwestChild);
+
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
+                expect(rootTile.data.waterMaskTexture).toBe(rootTile.southwestChild.data.waterMaskTexture);
             });
+        });
 
-            var imageryLayerCollection = new ImageryLayerCollection();
-
-            GlobeSurfaceTile.processStateMachine(tile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
+        it('creates water mask texture from multi-byte water mask data, if it exists', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .willHaveWaterMask(true, true, rootTile);
 
-            var layer = new ImageryLayer({
-                requestImage : function() {
-                    return when.reject();
-                }
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.data.waterMaskTexture).toBeDefined();
             });
-            var imagery = new Imagery(layer, 0, 0, 1, Rectangle.MAX_VALUE);
-            tile.data.imagery.push(new TileImagery(imagery, new Cartesian4()));
+        });
 
-            expect(imagery.parent.state).toBe(ImageryState.UNLOADED);
+        it('upsamples water mask if data is not available', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .willHaveWaterMask(false, true, rootTile)
+                .requestTileGeometryWillSucceed(rootTile.southwestChild);
 
-            return pollToPromise(function() {
-                GlobeSurfaceTile.processStateMachine(tile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []);
-                return imagery.parent.state !== ImageryState.UNLOADED;
+            return processor.process([rootTile, rootTile.southwestChild]).then(function() {
+                expect(rootTile.southwestChild.data.waterMaskTexture).toBeDefined();
+                expect(rootTile.southwestChild.data.waterMaskTranslationAndScale).toEqual(new Cartesian4(0.0, 0.0, 0.5, 0.5));
             });
         });
-    }, 'WebGL');
+    });
 
     describe('pick', function() {
         var scene;
@@ -543,28 +319,87 @@ defineSuite([
                 y : 1336
             });
 
-            var imageryLayerCollection = new ImageryLayerCollection();
-
-            return pollToPromise(function() {
-                if (!terrainProvider.ready) {
-                    return false;
-                }
-
-                // We know this tile is available, so we don't need to load the level 0 and level 10
-                //  tile to compute the availability.
-                terrainProvider._availability.addAvailableTileRange(11, 3788, 1336, 3788, 1336);
+            processor.frameState = scene.frameState;
+            processor.terrainProvider = terrainProvider;
 
-                GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, imageryLayerCollection, []);
-                RequestScheduler.update();
-                return tile.state === QuadtreeTileLoadState.DONE;
-            }).then(function() {
+            return processor.process([tile]).then(function() {
                 var ray = new Ray(
                     new Cartesian3(-5052039.459789615, 2561172.040315167, -2936276.999965875),
                     new Cartesian3(0.5036332963145244, 0.6648033332898124, 0.5517155343926082));
-                var pickResult = tile.data.pick(ray, undefined, true);
+                var pickResult = tile.data.pick(ray, undefined, undefined, true);
                 var cartographic = Ellipsoid.WGS84.cartesianToCartographic(pickResult);
                 expect(cartographic.height).toBeGreaterThan(-500.0);
             });
         });
     }, 'WebGL');
+
+    describe('eligibleForUnloading', function() {
+        beforeEach(function() {
+            processor.mockWebGL();
+        });
+
+        it('returns true when no loading has been done', function() {
+            rootTile.data = new GlobeSurfaceTile();
+            expect(rootTile.data.eligibleForUnloading).toBe(true);
+        });
+
+        it('returns true when some loading has been done', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile);
+
+            return processor.process([rootTile]).then(function() {
+                expect(rootTile.data.eligibleForUnloading).toBe(true);
+                mockTerrain
+                    .createMeshWillSucceed(rootTile);
+                return processor.process([rootTile]);
+            }).then(function() {
+                expect(rootTile.data.eligibleForUnloading).toBe(true);
+            });
+        });
+
+        it('returns false when RECEIVING', function() {
+            var deferred = when.defer();
+
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .requestTileGeometryWillWaitOn(deferred.promise, rootTile);
+
+            return processor.process([rootTile], 5).then(function() {
+                expect(rootTile.data.eligibleForUnloading).toBe(false);
+                deferred.resolve();
+            });
+        });
+
+        it ('returns false when TRANSFORMING', function() {
+            var deferred = when.defer();
+
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile)
+                .createMeshWillSucceed(rootTile)
+                .createMeshWillWaitOn(deferred.promise, rootTile);
+
+            return processor.process([rootTile], 5).then(function() {
+                expect(rootTile.data.eligibleForUnloading).toBe(false);
+                deferred.resolve();
+            });
+        });
+
+        it('returns false when imagery is TRANSITIONING', function() {
+            var deferred = when.defer();
+
+            var mockImagery = new MockImageryProvider();
+            imageryLayerCollection.addImageryProvider(mockImagery);
+
+            mockImagery
+                .requestImageWillWaitOn(deferred.promise, rootTile);
+
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTile);
+
+            return processor.process([rootTile], 5).then(function() {
+                expect(rootTile.data.eligibleForUnloading).toBe(false);
+                deferred.resolve();
+            });
+        });
+    });
 });
diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js
index 13233757aea..6dfc7b1550f 100644
--- a/Specs/Scene/ModelSpec.js
+++ b/Specs/Scene/ModelSpec.js
@@ -3231,7 +3231,6 @@ defineSuite([
                         tilesWaitingForChildren : 0
                     }
                 },
-                tileLoadedEvent : new Event(),
                 imageryLayersUpdatedEvent : new Event(),
                 destroy : function() {}
             };
diff --git a/Specs/Scene/QuadtreePrimitiveSpec.js b/Specs/Scene/QuadtreePrimitiveSpec.js
index e9d23d794bb..1a25e54913a 100644
--- a/Specs/Scene/QuadtreePrimitiveSpec.js
+++ b/Specs/Scene/QuadtreePrimitiveSpec.js
@@ -2,530 +2,1029 @@ defineSuite([
         'Scene/QuadtreePrimitive',
         'Core/Cartesian3',
         'Core/Cartographic',
+        'Core/defined',
         'Core/defineProperties',
+        'Core/Ellipsoid',
         'Core/EventHelper',
+        'Core/GeographicProjection',
         'Core/GeographicTilingScheme',
+        'Core/Intersect',
+        'Core/Rectangle',
         'Core/Visibility',
+        'Scene/Camera',
+        'Scene/GlobeSurfaceTileProvider',
+        'Scene/ImageryLayerCollection',
         'Scene/QuadtreeTileLoadState',
+        'Scene/SceneMode',
         'Specs/createScene',
-        'Specs/pollToPromise'
+        'Specs/pollToPromise',
+        'ThirdParty/when',
+        '../MockTerrainProvider',
+        '../TerrainTileProcessor'
     ], function(
         QuadtreePrimitive,
         Cartesian3,
         Cartographic,
+        defined,
         defineProperties,
+        Ellipsoid,
         EventHelper,
+        GeographicProjection,
         GeographicTilingScheme,
+        Intersect,
+        Rectangle,
         Visibility,
+        Camera,
+        GlobeSurfaceTileProvider,
+        ImageryLayerCollection,
         QuadtreeTileLoadState,
+        SceneMode,
         createScene,
-        pollToPromise) {
+        pollToPromise,
+        when,
+        MockTerrainProvider,
+        TerrainTileProcessor) {
     'use strict';
 
-    var scene;
+    describe('selectTilesForRendering', function() {
+        var scene;
+        var camera;
+        var frameState;
+        var quadtree;
+        var mockTerrain;
+        var tileProvider;
+        var imageryLayerCollection;
+        var surfaceShaderSet;
+        var processor;
+        var rootTiles;
+
+        beforeEach(function() {
+            scene = {
+                mapProjection: new GeographicProjection(),
+                drawingBufferWidth: 1000,
+                drawingBufferHeight: 1000
+            };
 
-    beforeAll(function() {
-        scene = createScene();
-        scene.render();
-    });
+            camera = new Camera(scene);
+
+            frameState = {
+                frameNumber: 0,
+                passes: {
+                    render: true
+                },
+                camera: camera,
+                fog: {
+                    enabled: false
+                },
+                context: {
+                    drawingBufferWidth: scene.drawingBufferWidth,
+                    drawingBufferHeight: scene.drawingBufferHeight
+                },
+                mode: SceneMode.SCENE3D,
+                commandList: [],
+                cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']),
+                afterRender: []
+            };
 
-    afterAll(function() {
-        scene.destroyForSpecs();
-    });
+            frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING);
 
-    it('must be constructed with a tileProvider', function() {
-        expect(function() {
-            return new QuadtreePrimitive();
-        }).toThrowDeveloperError();
+            imageryLayerCollection = new ImageryLayerCollection();
+            surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']);
+            mockTerrain = new MockTerrainProvider();
+            tileProvider = new GlobeSurfaceTileProvider({
+                terrainProvider: mockTerrain,
+                imageryLayers: imageryLayerCollection,
+                surfaceShaderSet: surfaceShaderSet
+            });
+            quadtree = new QuadtreePrimitive({
+                tileProvider: tileProvider
+            });
 
-        expect(function() {
-            return new QuadtreePrimitive({});
-        }).toThrowDeveloperError();
-    });
+            processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection);
+
+            quadtree.render(frameState);
+            rootTiles = quadtree._levelZeroTiles;
 
-    function createSpyTileProvider() {
-        var result = jasmine.createSpyObj('tileProvider', [
-            'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent',
-            'initialize', 'updateImagery', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile',
-            'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'isDestroyed', 'destroy']);
-
-        defineProperties(result, {
-            quadtree : {
-                get : result.getQuadtree,
-                set : result.setQuadtree
-            },
-            ready : {
-                get : result.getReady
-            },
-            tilingScheme : {
-                get : result.getTilingScheme
-            },
-            errorEvent : {
-                get : result.getErrorEvent
+            processor.mockWebGL();
+        });
+
+        function process(quadtreePrimitive, callback) {
+            var deferred = when.defer();
+
+            function next() {
+                ++frameState.frameNumber;
+                quadtree.beginFrame(frameState);
+                quadtree.render(frameState);
+                quadtree.endFrame(frameState);
+
+                if (callback()) {
+                    setTimeout(next, 0);
+                } else {
+                    deferred.resolve();
+                }
             }
+
+            next();
+
+            return deferred.promise;
+        }
+
+        it('must be constructed with a tileProvider', function() {
+            expect(function() {
+                return new QuadtreePrimitive();
+            }).toThrowDeveloperError();
+
+            expect(function() {
+                return new QuadtreePrimitive({});
+            }).toThrowDeveloperError();
         });
 
-        var tilingScheme = new GeographicTilingScheme();
-        result.getTilingScheme.and.returnValue(tilingScheme);
+        it('selects nothing when the root tiles are not yet ready', function() {
+            quadtree.render(frameState);
+            expect(quadtree._tilesToRender.length).toBe(0);
+        });
 
-        return result;
-    }
+        it('selects root tiles once they are ready', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTiles[0])
+                .requestTileGeometryWillSucceed(rootTiles[1])
+                .createMeshWillSucceed(rootTiles[0])
+                .createMeshWillSucceed(rootTiles[1]);
+
+            return processor.process(rootTiles).then(function() {
+                quadtree.render(frameState);
 
-    it('calls initialize, beginUpdate, loadTile, and endUpdate', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
+                // There should be at least one selected tile.
+                expect(quadtree._tilesToRender.length).toBeGreaterThan(0);
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
+                // All selected tiles should be root tiles.
+                expect(quadtree._tilesToRender.filter(function(tile) { return tile.level === 0; }).length).toBe(quadtree._tilesToRender.length);
+            });
         });
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        expect(tileProvider.initialize).toHaveBeenCalled();
-        expect(tileProvider.beginUpdate).toHaveBeenCalled();
-        expect(tileProvider.loadTile).toHaveBeenCalled();
-        expect(tileProvider.endUpdate).toHaveBeenCalled();
-    });
+        it('selects deeper tiles once they are renderable', function() {
+            mockTerrain
+                .requestTileGeometryWillSucceed(rootTiles[0])
+                .requestTileGeometryWillSucceed(rootTiles[1])
+                .createMeshWillSucceed(rootTiles[0])
+                .createMeshWillSucceed(rootTiles[1]);
+
+            rootTiles[0].children.forEach(function(tile) {
+                mockTerrain
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+                expect(tile.renderable).toBe(false);
+            });
+
+            return processor.process(rootTiles).then(function() {
+                quadtree.render(frameState);
+
+                // All selected tiles should be root tiles.
+                expect(quadtree._tilesToRender.length).toBeGreaterThan(0);
+                expect(quadtree._tilesToRender.filter(function(tile) { return tile.level === 0; }).length).toBe(quadtree._tilesToRender.length);
+
+                // Allow the child tiles to load.
+                return processor.process(rootTiles[0].children);
+            }).then(function() {
+                quadtree.render(frameState);
+
+                // Now child tiles should be rendered too.
+                expect(quadtree._tilesToRender).toContain(rootTiles[0].southwestChild);
+                expect(quadtree._tilesToRender).toContain(rootTiles[0].southeastChild);
+                expect(quadtree._tilesToRender).toContain(rootTiles[0].northwestChild);
+                expect(quadtree._tilesToRender).toContain(rootTiles[0].northeastChild);
+            });
+        });
 
-    it('shows the root tiles when they are ready and visible', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            tile.renderable = true;
+        it('skips loading levels when tiles are known to be available', function() {
+            // Mark all tiles through level 2 as available.
+            rootTiles.forEach(function(tile) {
+                // level 0 tile
+                mockTerrain
+                    .willBeAvailable(tile)
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+
+                tile.children.forEach(function(tile) {
+                    // level 1 tile
+                    mockTerrain
+                        .willBeAvailable(tile)
+                        .requestTileGeometryWillSucceed(tile)
+                        .createMeshWillSucceed(tile);
+
+                    tile.children.forEach(function(tile) {
+                        // level 2 tile
+                        mockTerrain
+                            .willBeAvailable(tile)
+                            .requestTileGeometryWillSucceed(tile)
+                            .createMeshWillSucceed(tile);
+                    });
+                });
+            });
+
+            quadtree.preloadAncestors = false;
+
+            // Look down at the center of a level 2 tile from a distance that will refine to it.
+            var lookAtTile = rootTiles[0].southwestChild.northeastChild;
+            setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level);
+
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return process(quadtree, function() {
+                // Process until the lookAtTile is rendered. That tile's parent (level 1)
+                // should not be rendered along the way.
+                expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
+                var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
+                var continueProcessing = !lookAtTileRendered;
+                return continueProcessing;
+            }).then(function() {
+                // The lookAtTile should be a real tile, not a fill.
+                expect(quadtree._tilesToRender).toContain(lookAtTile);
+                expect(lookAtTile.data.fill).toBeUndefined();
+                expect(lookAtTile.data.vertexArray).toBeDefined();
+
+                // The parent of the lookAtTile should not have been requested.
+                var parent = lookAtTile.parent;
+                mockTerrain.requestTileGeometry.calls.allArgs().forEach(function(call) {
+                    expect(call.slice(0, 3)).not.toEqual([parent.x, parent.y, parent.level]);
+                });
+            });
         });
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
+        it('does not skip loading levels if availability is unknown', function() {
+            // Mark all tiles through level 2 as available.
+            rootTiles.forEach(function(tile) {
+                // level 0 tile
+                mockTerrain
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+
+                tile.children.forEach(function(tile) {
+                    // level 1 tile
+                    mockTerrain
+                        .willBeAvailable(tile)
+                        .requestTileGeometryWillSucceed(tile)
+                        .createMeshWillSucceed(tile);
+
+                    tile.children.forEach(function(tile) {
+                        // level 2 tile
+                        mockTerrain
+                            .willBeUnknownAvailability(tile)
+                            .requestTileGeometryWillSucceed(tile)
+                            .createMeshWillSucceed(tile);
+                    });
+                });
+            });
+
+            quadtree.preloadAncestors = false;
+
+            // Look down at the center of a level 2 tile from a distance that will refine to it.
+            var lookAtTile = rootTiles[0].southwestChild.northeastChild;
+            setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level);
+
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return process(quadtree, function() {
+                // Process until the lookAtTile is rendered. That tile's parent (level 1)
+                // should not be rendered along the way, but it will be loaded.
+                expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
+                var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
+                var continueProcessing = !lookAtTileRendered;
+                return continueProcessing;
+            }).then(function() {
+                // The lookAtTile should be a real tile, not a fill.
+                expect(quadtree._tilesToRender).toContain(lookAtTile);
+                expect(lookAtTile.data.fill).toBeUndefined();
+                expect(lookAtTile.data.vertexArray).toBeDefined();
+
+                // The parent of the lookAtTile should have been requested before the lookAtTile itself.
+                var parent = lookAtTile.parent;
+                var allArgs = mockTerrain.requestTileGeometry.calls.allArgs();
+                var parentArgsIndex = allArgs.indexOf(allArgs.filter(function(call) {
+                    return call[0] === parent.x && call[1] === parent.y && call[2] === parent.level;
+                })[0]);
+                var lookAtArgsIndex = allArgs.indexOf(allArgs.filter(function(call) {
+                    return call[0] === lookAtTile.x && call[1] === lookAtTile.y && call[2] === lookAtTile.level;
+                })[0]);
+                expect(parentArgsIndex).toBeLessThan(lookAtArgsIndex);
+            });
         });
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+        it('loads and renders intermediate tiles according to loadingDescendantLimit', function() {
+            // Mark all tiles through level 2 as available.
+            rootTiles.forEach(function(tile) {
+                // level 0 tile
+                mockTerrain
+                    .willBeAvailable(tile)
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+
+                tile.children.forEach(function(tile) {
+                    // level 1 tile
+                    mockTerrain
+                        .willBeAvailable(tile)
+                        .requestTileGeometryWillSucceed(tile)
+                        .createMeshWillSucceed(tile);
+
+                    tile.children.forEach(function(tile) {
+                        // level 2 tile
+                        mockTerrain
+                            .willBeAvailable(tile)
+                            .requestTileGeometryWillSucceed(tile)
+                            .createMeshWillSucceed(tile);
+                    });
+                });
+            });
+
+            quadtree.preloadAncestors = false;
+            quadtree.loadingDescendantLimit = 1;
+
+            // Look down at the center of a level 2 tile from a distance that will refine to it.
+            var lookAtTile = rootTiles[0].southwestChild.northeastChild;
+            setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level);
+
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return process(quadtree, function() {
+                // First the lookAtTile's parent should be rendered.
+                var lookAtTileParentRendered = quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0;
+                var continueProcessing = !lookAtTileParentRendered;
+                return continueProcessing;
+            }).then(function() {
+                // The lookAtTile's parent should be a real tile, not a fill.
+                expect(quadtree._tilesToRender).toContain(lookAtTile.parent);
+                expect(lookAtTile.parent.data.fill).toBeUndefined();
+                expect(lookAtTile.parent.data.vertexArray).toBeDefined();
+
+                return process(quadtree, function() {
+                    // Then the lookAtTile should be rendered.
+                    var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
+                    var continueProcessing = !lookAtTileRendered;
+                    return continueProcessing;
+                });
+            }).then(function() {
+                // The lookAtTile should be a real tile, not a fill.
+                expect(quadtree._tilesToRender).toContain(lookAtTile);
+                expect(lookAtTile.data.fill).toBeUndefined();
+                expect(lookAtTile.data.vertexArray).toBeDefined();
+            });
+        });
+
+        it('continues rendering more detailed tiles when camera zooms out and an appropriate ancestor is not yet renderable', function() {
+            // Mark all tiles through level 2 as available.
+            rootTiles.forEach(function(tile) {
+                // level 0 tile
+                mockTerrain
+                    .willBeAvailable(tile)
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+
+                tile.children.forEach(function(tile) {
+                    // level 1 tile
+                    mockTerrain
+                        .willBeAvailable(tile)
+                        .requestTileGeometryWillSucceed(tile)
+                        .createMeshWillSucceed(tile);
+
+                    tile.children.forEach(function(tile) {
+                        // level 2 tile
+                        mockTerrain
+                            .willBeAvailable(tile)
+                            .requestTileGeometryWillSucceed(tile)
+                            .createMeshWillSucceed(tile);
+                    });
+                });
+            });
+
+            quadtree.preloadAncestors = false;
+
+            // Look down at the center of a level 2 tile from a distance that will refine to it.
+            var lookAtTile = rootTiles[0].southwestChild.northeastChild;
+            setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.level);
+
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return process(quadtree, function() {
+                // Process until the lookAtTile is rendered. That tile's parent (level 1)
+                // should not be rendered along the way.
+                expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
+                var lookAtTileRendered = quadtree._tilesToRender.indexOf(lookAtTile) >= 0;
+                var continueProcessing = !lookAtTileRendered;
+                return continueProcessing;
+            }).then(function() {
+                // Zoom out so the parent tile no longer needs to refine to meet SSE.
+                setCameraPosition(quadtree, frameState, Rectangle.center(lookAtTile.rectangle), lookAtTile.parent.level);
+
+                // Select new tiles
+                quadtree.beginFrame(frameState);
+                quadtree.render(frameState);
+                quadtree.endFrame(frameState);
+
+                // The lookAtTile should still be rendered, not it's parent.
+                expect(quadtree._tilesToRender).toContain(lookAtTile);
+                expect(quadtree._tilesToRender).not.toContain(lookAtTile.parent);
+
+                return process(quadtree, function() {
+                    // Eventually the parent should be rendered instead.
+                    var parentRendered = quadtree._tilesToRender.indexOf(lookAtTile.parent) >= 0;
+                    var continueProcessing = !parentRendered;
+                    return continueProcessing;
+                });
+            }).then(function() {
+                expect(quadtree._tilesToRender).not.toContain(lookAtTile);
+                expect(quadtree._tilesToRender).toContain(lookAtTile.parent);
+            });
+        });
 
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+        it('renders a fill for a newly-visible tile', function() {
+            // Mark all tiles through level 2 as available.
+            rootTiles.forEach(function(tile) {
+                // level 0 tile
+                mockTerrain
+                    .willBeAvailable(tile)
+                    .requestTileGeometryWillSucceed(tile)
+                    .createMeshWillSucceed(tile);
+
+                tile.children.forEach(function(tile) {
+                    // level 1 tile
+                    mockTerrain
+                        .willBeAvailable(tile)
+                        .requestTileGeometryWillSucceed(tile)
+                        .createMeshWillSucceed(tile);
+
+                    tile.children.forEach(function(tile) {
+                        // level 2 tile
+                        mockTerrain
+                            .willBeAvailable(tile)
+                            .requestTileGeometryWillSucceed(tile)
+                            .createMeshWillSucceed(tile);
+                    });
+                });
+            });
+
+            quadtree.preloadAncestors = false;
+
+            var visibleTile = rootTiles[0].southwestChild.northeastChild;
+            var notVisibleTile = rootTiles[0].southwestChild.northwestChild;
+
+            frameState.cullingVolume.computeVisibility.and.callFake(function(boundingVolume) {
+                if (!defined(visibleTile.data)) {
+                    return Intersect.INTERSECTING;
+                }
 
-        expect(tileProvider.showTileThisFrame).toHaveBeenCalled();
+                if (boundingVolume === visibleTile.data.orientedBoundingBox) {
+                    return Intersect.INTERSECTING;
+                } else if (boundingVolume === notVisibleTile.data.orientedBoundingBox) {
+                    return Intersect.OUTSIDE;
+                }
+                return Intersect.INTERSECTING;
+            });
+
+            // Look down at the center of the visible tile.
+            setCameraPosition(quadtree, frameState, Rectangle.center(visibleTile.rectangle), visibleTile.level);
+
+            spyOn(mockTerrain, 'requestTileGeometry').and.callThrough();
+
+            return process(quadtree, function() {
+                // Process until the visibleTile is rendered.
+                var visibleTileRendered = quadtree._tilesToRender.indexOf(visibleTile) >= 0;
+                var continueProcessing = !visibleTileRendered;
+                return continueProcessing;
+            }).then(function() {
+                expect(quadtree._tilesToRender).not.toContain(notVisibleTile);
+
+                // Now treat the not-visible-tile as visible.
+                frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING);
+
+                // Select new tiles
+                quadtree.beginFrame(frameState);
+                quadtree.render(frameState);
+                quadtree.endFrame(frameState);
+
+                // The notVisibleTile should be rendered as a fill.
+                expect(quadtree._tilesToRender).toContain(visibleTile);
+                expect(quadtree._tilesToRender).toContain(notVisibleTile);
+                expect(notVisibleTile.data.fill).toBeDefined();
+                expect(notVisibleTile.data.vertexArray).toBeUndefined();
+            });
+        });
     });
 
-    it('stops loading a tile that moves to the DONE state', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+    describe('with mock tile provider', function() {
+        var scene;
 
-        var calls = 0;
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            ++calls;
-            tile.state = QuadtreeTileLoadState.DONE;
+        beforeAll(function() {
+            scene = createScene();
+            scene.render();
         });
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
+        afterAll(function() {
+            scene.destroyForSpecs();
         });
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+        function createSpyTileProvider() {
+            var result = jasmine.createSpyObj('tileProvider', [
+                'getQuadtree', 'setQuadtree', 'getReady', 'getTilingScheme', 'getErrorEvent',
+                'initialize', 'updateImagery', 'beginUpdate', 'endUpdate', 'getLevelMaximumGeometricError', 'loadTile',
+                'computeTileVisibility', 'showTileThisFrame', 'computeDistanceToTile', 'canRefine', 'isDestroyed', 'destroy']);
+
+            defineProperties(result, {
+                quadtree : {
+                    get : result.getQuadtree,
+                    set : result.setQuadtree
+                },
+                ready : {
+                    get : result.getReady
+                },
+                tilingScheme : {
+                    get : result.getTilingScheme
+                },
+                errorEvent : {
+                    get : result.getErrorEvent
+                }
+            });
 
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            var tilingScheme = new GeographicTilingScheme();
+            result.getTilingScheme.and.returnValue(tilingScheme);
 
-        expect(calls).toBe(2);
+            result.canRefine.and.callFake(function(tile) {
+                return tile.renderable;
+            });
 
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            return result;
+        }
 
-        expect(calls).toBe(2);
-    });
+        it('calls initialize, beginUpdate, loadTile, and endUpdate', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
 
-    it('tileLoadProgressEvent is raised when tile loaded and when new children discovered', function() {
-        var eventHelper = new EventHelper();
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            expect(tileProvider.initialize).toHaveBeenCalled();
+            expect(tileProvider.beginUpdate).toHaveBeenCalled();
+            expect(tileProvider.loadTile).toHaveBeenCalled();
+            expect(tileProvider.endUpdate).toHaveBeenCalled();
         });
 
-        var progressEventSpy = jasmine.createSpy('progressEventSpy');
-        eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy);
+        it('shows the root tiles when they are ready and visible', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                tile.renderable = true;
+            });
 
-        // Initial update to get the zero-level tiles set up.
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-        // load zero-level tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        quadtree.update(scene.frameState);
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        scene.renderForSpecs();
+            expect(tileProvider.showTileThisFrame).toHaveBeenCalled();
+        });
 
-        // There will now be two zero-level tiles in the load queue.
-        expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2);
+        it('stops loading a tile that moves to the DONE state', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
 
-        // Change one to loaded and update again
-        quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE;
-        quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING;
+            var calls = 0;
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                ++calls;
+                tile.state = QuadtreeTileLoadState.DONE;
+            });
 
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-        quadtree.update(scene.frameState);
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        scene.renderForSpecs();
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        // Now there should only be one left in the update queue
-        expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1);
+            expect(calls).toBe(2);
 
-        // Simulate the second zero-level child having loaded with two children.
-        quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE;
-        quadtree._levelZeroTiles[1].renderable = true;
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            expect(calls).toBe(2);
+        });
 
-        quadtree.update(scene.frameState);
+        it('tileLoadProgressEvent is raised when tile loaded and when new children discovered', function() {
+            var eventHelper = new EventHelper();
 
-        scene.renderForSpecs();
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
 
-        // Now that tile's four children should be in the load queue.
-        expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4);
-    });
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-    it('forEachLoadedTile does not enumerate tiles in the START state', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
-        tileProvider.computeDistanceToTile.and.returnValue(1e-15);
+            var progressEventSpy = jasmine.createSpy('progressEventSpy');
+            eventHelper.add(quadtree.tileLoadProgressEvent, progressEventSpy);
 
-        // Load the root tiles.
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            tile.state = QuadtreeTileLoadState.DONE;
-            tile.renderable = true;
-        });
+            // Initial update to get the zero-level tiles set up.
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
-        });
+            // load zero-level tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // Don't load further tiles.
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            tile.state = QuadtreeTileLoadState.START;
-        });
+            quadtree.update(scene.frameState);
 
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            scene.renderForSpecs();
 
-        quadtree.forEachLoadedTile(function(tile) {
-            expect(tile.state).not.toBe(QuadtreeTileLoadState.START);
-        });
-    });
+            // There will now be two zero-level tiles in the load queue.
+            expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(2);
 
-    it('add and remove callbacks to tiles', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
-        tileProvider.computeDistanceToTile.and.returnValue(1e-15);
-
-        // Load the root tiles.
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            tile.state = QuadtreeTileLoadState.DONE;
-            tile.renderable = true;
-            tile.data = {
-                pick : function() {
-                    return undefined;
-                }
-            };
-        });
+            // Change one to loaded and update again
+            quadtree._levelZeroTiles[0].state = QuadtreeTileLoadState.DONE;
+            quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.LOADING;
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
-        });
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) {
-        });
+            quadtree.update(scene.frameState);
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        var addedCallback = false;
-        quadtree.forEachLoadedTile(function(tile) {
-            addedCallback = addedCallback || tile.customData.length > 0;
-        });
+            scene.renderForSpecs();
 
-        expect(addedCallback).toEqual(true);
+            // Now there should only be one left in the update queue
+            expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(1);
 
-        removeFunc();
+            // Simulate the second zero-level child having loaded with two children.
+            quadtree._levelZeroTiles[1].state = QuadtreeTileLoadState.DONE;
+            quadtree._levelZeroTiles[1].renderable = true;
 
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            quadtree.update(scene.frameState);
 
-        var removedCallback = true;
-        quadtree.forEachLoadedTile(function(tile) {
-            removedCallback = removedCallback && tile.customData.length === 0;
+            scene.renderForSpecs();
+
+            // Now that tile's four children should be in the load queue.
+            expect(progressEventSpy.calls.mostRecent().args[0]).toEqual(4);
         });
 
-        expect(removedCallback).toEqual(true);
-    });
+        it('forEachLoadedTile does not enumerate tiles in the START state', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            tileProvider.computeDistanceToTile.and.returnValue(1e-15);
 
-    it('updates heights', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
-        tileProvider.computeDistanceToTile.and.returnValue(1e-15);
+            // Load the root tiles.
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                tile.state = QuadtreeTileLoadState.DONE;
+                tile.renderable = true;
+            });
 
-        tileProvider.terrainProvider = {
-            getTileDataAvailable : function() {
-                return true;
-            }
-        };
-
-        var position = Cartesian3.clone(Cartesian3.ZERO);
-        var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X);
-        var currentPosition = position;
-
-        // Load the root tiles.
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            tile.state = QuadtreeTileLoadState.DONE;
-            tile.renderable = true;
-            tile.data = {
-                pick : function() {
-                    return currentPosition;
-                }
-            };
-        });
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
-        });
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) {
-            Cartesian3.clone(p, position);
+            // Don't load further tiles.
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                tile.state = QuadtreeTileLoadState.START;
+            });
+
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            quadtree.forEachLoadedTile(function(tile) {
+                expect(tile.state).not.toBe(QuadtreeTileLoadState.START);
+            });
         });
 
-        // determine what tiles to load
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+        it('add and remove callbacks to tiles', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            tileProvider.computeDistanceToTile.and.returnValue(1e-15);
 
-        // load tiles
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            // Load the root tiles.
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                tile.state = QuadtreeTileLoadState.DONE;
+                tile.renderable = true;
+                tile.data = {
+                    pick : function() {
+                        return undefined;
+                    }
+                };
+            });
 
-        expect(position).toEqual(Cartesian3.ZERO);
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
 
-        currentPosition = updatedPosition;
+            var removeFunc = quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(position) {
+            });
 
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        expect(position).toEqual(updatedPosition);
-    });
+            ++scene.frameState.frameNumber;
+
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-    it('gives correct priority to tile loads', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            var addedCallback = false;
+            quadtree.forEachLoadedTile(function(tile) {
+                addedCallback = addedCallback || tile.customData.length > 0;
+            });
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
-        });
+            expect(addedCallback).toEqual(true);
 
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // The root tiles should be in the high priority load queue
-        expect(quadtree._tileLoadQueueHigh.length).toBe(2);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
-        expect(quadtree._tileLoadQueueMedium.length).toBe(0);
-        expect(quadtree._tileLoadQueueLow.length).toBe(0);
-
-        // Mark the first root tile renderable (but not done loading)
-        quadtree._levelZeroTiles[0].renderable = true;
-
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // That root tile should now load with low priority while its children should load with high.
-        expect(quadtree._tileLoadQueueHigh.length).toBe(5);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[1]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[2]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[3]);
-        expect(quadtree._tileLoadQueueMedium.length).toBe(0);
-        expect(quadtree._tileLoadQueueLow.length).toBe(1);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
-
-        // Mark the children of that root tile renderable too, so we can refine it
-        quadtree._levelZeroTiles[0].children[0].renderable = true;
-        quadtree._levelZeroTiles[0].children[1].renderable = true;
-        quadtree._levelZeroTiles[0].children[2].renderable = true;
-        quadtree._levelZeroTiles[0].children[3].renderable = true;
-
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[0]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[1]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[2]);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[3]);
-        expect(quadtree._tileLoadQueueMedium.length).toBe(0);
-        expect(quadtree._tileLoadQueueLow.length).toBe(5);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]);
-
-        // Mark the children of levelZeroTiles[0] upsampled
-        quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true;
-        quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true;
-        quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true;
-        quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true;
-
-        quadtree.update(scene.frameState);
-        quadtree.beginFrame(scene.frameState);
-        quadtree.render(scene.frameState);
-        quadtree.endFrame(scene.frameState);
-
-        // levelZeroTiles[0] should move to medium priority.
-        // Its descendents should continue loading, so they have a chance to decide they're not upsampled later.
-        expect(quadtree._tileLoadQueueHigh.length).toBe(1);
-        expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
-        expect(quadtree._tileLoadQueueMedium.length).toBe(1);
-        expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]);
-        expect(quadtree._tileLoadQueueLow.length).toBe(4);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]);
-        expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]);
-    });
+            removeFunc();
 
-    it('renders tiles in approximate near-to-far order', function() {
-        var tileProvider = createSpyTileProvider();
-        tileProvider.getReady.and.returnValue(true);
-        tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            ++scene.frameState.frameNumber;
 
-        var quadtree = new QuadtreePrimitive({
-            tileProvider : tileProvider
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            var removedCallback = true;
+            quadtree.forEachLoadedTile(function(tile) {
+                removedCallback = removedCallback && tile.customData.length === 0;
+            });
+
+            expect(removedCallback).toEqual(true);
         });
 
-        tileProvider.loadTile.and.callFake(function(frameState, tile) {
-            if (tile.level <= 1) {
+        it('updates heights', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+            tileProvider.computeDistanceToTile.and.returnValue(1e-15);
+
+            tileProvider.terrainProvider = {
+                getTileDataAvailable : function() {
+                    return true;
+                }
+            };
+
+            var position = Cartesian3.clone(Cartesian3.ZERO);
+            var updatedPosition = Cartesian3.clone(Cartesian3.UNIT_X);
+            var currentPosition = position;
+
+            // Load the root tiles.
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
                 tile.state = QuadtreeTileLoadState.DONE;
                 tile.renderable = true;
-            }
-        });
+                tile.data = {
+                    pick : function() {
+                        return currentPosition;
+                    },
+                    mesh: {}
+                };
+            });
+
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
+
+            quadtree.updateHeight(Cartographic.fromDegrees(-72.0, 40.0), function(p) {
+                Cartesian3.clone(p, position);
+            });
+
+            // determine what tiles to load
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
 
-        scene.camera.setView({
-            destination : Cartesian3.fromDegrees(1.0, 1.0, 15000.0)
+            // load tiles
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            expect(position).toEqual(Cartesian3.ZERO);
+
+            currentPosition = updatedPosition;
+
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            expect(position).toEqual(updatedPosition);
         });
-        scene.camera.update(scene.mode);
 
-        return pollToPromise(function() {
+        it('gives correct priority to tile loads', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
+
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            // The root tiles should be in the high priority load queue
+            expect(quadtree._tileLoadQueueHigh.length).toBe(2);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
+            expect(quadtree._tileLoadQueueMedium.length).toBe(0);
+            expect(quadtree._tileLoadQueueLow.length).toBe(0);
+
+            // Mark the first root tile renderable (but not done loading)
+            quadtree._levelZeroTiles[0].renderable = true;
+
+            quadtree.update(scene.frameState);
+            quadtree.beginFrame(scene.frameState);
+            quadtree.render(scene.frameState);
+            quadtree.endFrame(scene.frameState);
+
+            // That root tile should now load with low priority while its children should load with high.
+            expect(quadtree._tileLoadQueueHigh.length).toBe(5);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[1]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[2]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[3]);
+            expect(quadtree._tileLoadQueueMedium.length).toBe(0);
+            expect(quadtree._tileLoadQueueLow.length).toBe(1);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
+
+            // Mark the children of that root tile renderable too, so we can refine it
+            quadtree._levelZeroTiles[0].children[0].renderable = true;
+            quadtree._levelZeroTiles[0].children[1].renderable = true;
+            quadtree._levelZeroTiles[0].children[2].renderable = true;
+            quadtree._levelZeroTiles[0].children[3].renderable = true;
+
             quadtree.update(scene.frameState);
             quadtree.beginFrame(scene.frameState);
             quadtree.render(scene.frameState);
             quadtree.endFrame(scene.frameState);
 
-            return quadtree._tilesToRender.filter(function(tile) { return tile.level === 1; }).length === 8;
-        }).then(function() {
+            expect(quadtree._tileLoadQueueHigh.length).toBe(17); // levelZeroTiles[1] plus levelZeroTiles[0]'s 16 grandchildren
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[0]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[1]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[2]);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[0].children[0].children[3]);
+            expect(quadtree._tileLoadQueueMedium.length).toBe(0);
+            expect(quadtree._tileLoadQueueLow.length).toBe(5);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0]);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[0]);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[1]);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[2]);
+            expect(quadtree._tileLoadQueueLow).toContain(quadtree._levelZeroTiles[0].children[3]);
+
+            // Mark the children of levelZeroTiles[0] upsampled
+            quadtree._levelZeroTiles[0].children[0].upsampledFromParent = true;
+            quadtree._levelZeroTiles[0].children[1].upsampledFromParent = true;
+            quadtree._levelZeroTiles[0].children[2].upsampledFromParent = true;
+            quadtree._levelZeroTiles[0].children[3].upsampledFromParent = true;
+
             quadtree.update(scene.frameState);
             quadtree.beginFrame(scene.frameState);
             quadtree.render(scene.frameState);
             quadtree.endFrame(scene.frameState);
 
-            // Rendered tiles:
-            // +----+----+----+----+
-            // |w.nw|w.ne|e.nw|e.ne|
-            // +----+----+----+----+
-            // |w.sw|w.se|e.sw|e.se|
-            // +----+----+----+----+
-            // camera is located in e.nw (east.northwestChild)
-
-            var west = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 0; })[0];
-            var east = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 1; })[0];
-            expect(quadtree._tilesToRender[0]).toBe(east.northwestChild);
-            expect(quadtree._tilesToRender[1] === east.southwestChild || quadtree._tilesToRender[1] === east.northeastChild).toBe(true);
-            expect(quadtree._tilesToRender[2] === east.southwestChild || quadtree._tilesToRender[2] === east.northeastChild).toBe(true);
-            expect(quadtree._tilesToRender[3]).toBe(east.southeastChild);
-            expect(quadtree._tilesToRender[4]).toBe(west.northeastChild);
-            expect(quadtree._tilesToRender[5] === west.northwestChild || quadtree._tilesToRender[5] === west.southeastChild).toBe(true);
-            expect(quadtree._tilesToRender[6] === west.northwestChild || quadtree._tilesToRender[6] === west.southeastChild).toBe(true);
-            expect(quadtree._tilesToRender[7]).toBe(west.southwestChild);
+            // levelZeroTiles[0] should move to medium priority.
+            expect(quadtree._tileLoadQueueHigh.length).toBe(1);
+            expect(quadtree._tileLoadQueueHigh).toContain(quadtree._levelZeroTiles[1]);
+            expect(quadtree._tileLoadQueueMedium.length).toBe(1);
+            expect(quadtree._tileLoadQueueMedium).toContain(quadtree._levelZeroTiles[0]);
+            expect(quadtree._tileLoadQueueLow.length).toBe(0);
         });
-    });
-}, 'WebGL');
+
+        it('renders tiles in approximate near-to-far order', function() {
+            var tileProvider = createSpyTileProvider();
+            tileProvider.getReady.and.returnValue(true);
+            tileProvider.computeTileVisibility.and.returnValue(Visibility.FULL);
+
+            var quadtree = new QuadtreePrimitive({
+                tileProvider : tileProvider
+            });
+
+            tileProvider.loadTile.and.callFake(function(frameState, tile) {
+                if (tile.level <= 1) {
+                    tile.state = QuadtreeTileLoadState.DONE;
+                    tile.renderable = true;
+                }
+            });
+
+            scene.camera.setView({
+                destination : Cartesian3.fromDegrees(1.0, 1.0, 15000.0)
+            });
+            scene.camera.update(scene.mode);
+
+            return pollToPromise(function() {
+                quadtree.update(scene.frameState);
+                quadtree.beginFrame(scene.frameState);
+                quadtree.render(scene.frameState);
+                quadtree.endFrame(scene.frameState);
+
+                return quadtree._tilesToRender.filter(function(tile) { return tile.level === 1; }).length === 8;
+            }).then(function() {
+                quadtree.update(scene.frameState);
+                quadtree.beginFrame(scene.frameState);
+                quadtree.render(scene.frameState);
+                quadtree.endFrame(scene.frameState);
+
+                // Rendered tiles:
+                // +----+----+----+----+
+                // |w.nw|w.ne|e.nw|e.ne|
+                // +----+----+----+----+
+                // |w.sw|w.se|e.sw|e.se|
+                // +----+----+----+----+
+                // camera is located in e.nw (east.northwestChild)
+
+                var west = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 0; })[0];
+                var east = quadtree._levelZeroTiles.filter(function(tile) { return tile.x === 1; })[0];
+                expect(quadtree._tilesToRender[0]).toBe(east.northwestChild);
+                expect(quadtree._tilesToRender[1] === east.southwestChild || quadtree._tilesToRender[1] === east.northeastChild).toBe(true);
+                expect(quadtree._tilesToRender[2] === east.southwestChild || quadtree._tilesToRender[2] === east.northeastChild).toBe(true);
+                expect(quadtree._tilesToRender[3]).toBe(east.southeastChild);
+                expect(quadtree._tilesToRender[4]).toBe(west.northeastChild);
+                expect(quadtree._tilesToRender[5] === west.northwestChild || quadtree._tilesToRender[5] === west.southeastChild).toBe(true);
+                expect(quadtree._tilesToRender[6] === west.northwestChild || quadtree._tilesToRender[6] === west.southeastChild).toBe(true);
+                expect(quadtree._tilesToRender[7]).toBe(west.southwestChild);
+            });
+        });
+    }, 'WebGL');
+
+    // Sets the camera to look at a given cartographic position from a distance
+    // that will produce a screen-space error at that position that will refine to
+    // a given tile level and no further.
+    function setCameraPosition(quadtree, frameState, position, level) {
+        var camera = frameState.camera;
+        var geometricError = quadtree.tileProvider.getLevelMaximumGeometricError(level);
+        var sse = quadtree.maximumScreenSpaceError * 0.8;
+        var sseDenominator = camera.frustum.sseDenominator;
+        var height = frameState.context.drawingBufferHeight;
+
+        var distance = (geometricError * height) / (sse * sseDenominator);
+        var cartesian = Ellipsoid.WGS84.cartographicToCartesian(position);
+        camera.lookAt(cartesian, new Cartesian3(0.0, 0.0, distance));
+    }
+});
diff --git a/Specs/Scene/QuadtreeTileSpec.js b/Specs/Scene/QuadtreeTileSpec.js
index 7ff5af37bd7..6f01ff6d142 100644
--- a/Specs/Scene/QuadtreeTileSpec.js
+++ b/Specs/Scene/QuadtreeTileSpec.js
@@ -109,6 +109,198 @@ defineSuite([
         }).toThrowDeveloperError();
     });
 
+    it('can get tiles around a root tile', function() {
+        var tilingScheme = new GeographicTilingScheme({
+            numberOfLevelZeroTilesX : 3,
+            numberOfLevelZeroTilesY : 3
+        });
+        var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+
+        var L0X0Y0 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 0 && tile.y === 0;
+        })[0];
+        var L0X1Y0 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 1 && tile.y === 0;
+        })[0];
+        var L0X2Y0 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 2 && tile.y === 0;
+        })[0];
+        var L0X0Y1 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 0 && tile.y === 1;
+        })[0];
+        var L0X1Y1 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 1 && tile.y === 1;
+        })[0];
+        var L0X2Y1 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 2 && tile.y === 1;
+        })[0];
+        var L0X0Y2 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 0 && tile.y === 2;
+        })[0];
+        var L0X1Y2 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 1 && tile.y === 2;
+        })[0];
+        var L0X2Y2 = levelZeroTiles.filter(function(tile) {
+            return tile.x === 2 && tile.y === 2;
+        })[0];
+
+        expect(L0X0Y0.findTileToWest(levelZeroTiles)).toBe(L0X2Y0);
+        expect(L0X0Y0.findTileToEast(levelZeroTiles)).toBe(L0X1Y0);
+        expect(L0X0Y0.findTileToNorth(levelZeroTiles)).toBeUndefined();
+        expect(L0X0Y0.findTileToSouth(levelZeroTiles)).toBe(L0X0Y1);
+
+        expect(L0X1Y0.findTileToWest(levelZeroTiles)).toBe(L0X0Y0);
+        expect(L0X1Y0.findTileToEast(levelZeroTiles)).toBe(L0X2Y0);
+        expect(L0X1Y0.findTileToNorth(levelZeroTiles)).toBeUndefined();
+        expect(L0X1Y0.findTileToSouth(levelZeroTiles)).toBe(L0X1Y1);
+
+        expect(L0X2Y0.findTileToWest(levelZeroTiles)).toBe(L0X1Y0);
+        expect(L0X2Y0.findTileToEast(levelZeroTiles)).toBe(L0X0Y0);
+        expect(L0X2Y0.findTileToNorth(levelZeroTiles)).toBeUndefined();
+        expect(L0X2Y0.findTileToSouth(levelZeroTiles)).toBe(L0X2Y1);
+
+        expect(L0X0Y1.findTileToWest(levelZeroTiles)).toBe(L0X2Y1);
+        expect(L0X0Y1.findTileToEast(levelZeroTiles)).toBe(L0X1Y1);
+        expect(L0X0Y1.findTileToNorth(levelZeroTiles)).toBe(L0X0Y0);
+        expect(L0X0Y1.findTileToSouth(levelZeroTiles)).toBe(L0X0Y2);
+
+        expect(L0X1Y1.findTileToWest(levelZeroTiles)).toBe(L0X0Y1);
+        expect(L0X1Y1.findTileToEast(levelZeroTiles)).toBe(L0X2Y1);
+        expect(L0X1Y1.findTileToNorth(levelZeroTiles)).toBe(L0X1Y0);
+        expect(L0X1Y1.findTileToSouth(levelZeroTiles)).toBe(L0X1Y2);
+
+        expect(L0X2Y1.findTileToWest(levelZeroTiles)).toBe(L0X1Y1);
+        expect(L0X2Y1.findTileToEast(levelZeroTiles)).toBe(L0X0Y1);
+        expect(L0X2Y1.findTileToNorth(levelZeroTiles)).toBe(L0X2Y0);
+        expect(L0X2Y1.findTileToSouth(levelZeroTiles)).toBe(L0X2Y2);
+
+        expect(L0X0Y2.findTileToWest(levelZeroTiles)).toBe(L0X2Y2);
+        expect(L0X0Y2.findTileToEast(levelZeroTiles)).toBe(L0X1Y2);
+        expect(L0X0Y2.findTileToNorth(levelZeroTiles)).toBe(L0X0Y1);
+        expect(L0X0Y2.findTileToSouth(levelZeroTiles)).toBeUndefined();
+
+        expect(L0X1Y2.findTileToWest(levelZeroTiles)).toBe(L0X0Y2);
+        expect(L0X1Y2.findTileToEast(levelZeroTiles)).toBe(L0X2Y2);
+        expect(L0X1Y2.findTileToNorth(levelZeroTiles)).toBe(L0X1Y1);
+        expect(L0X1Y2.findTileToSouth(levelZeroTiles)).toBeUndefined();
+
+        expect(L0X2Y2.findTileToWest(levelZeroTiles)).toBe(L0X1Y2);
+        expect(L0X2Y2.findTileToEast(levelZeroTiles)).toBe(L0X0Y2);
+        expect(L0X2Y2.findTileToNorth(levelZeroTiles)).toBe(L0X2Y1);
+        expect(L0X2Y2.findTileToSouth(levelZeroTiles)).toBeUndefined();
+    });
+
+    it('can get tiles around a tile when they share a common parent', function() {
+        var tilingScheme = new GeographicTilingScheme({
+            numberOfLevelZeroTilesX : 2,
+            numberOfLevelZeroTilesY : 1
+        });
+
+        var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+        var parent = levelZeroTiles[1];
+        var sw = parent.southwestChild;
+        var se = parent.southeastChild;
+        var nw = parent.northwestChild;
+        var ne = parent.northeastChild;
+
+        expect(sw.findTileToEast(levelZeroTiles)).toBe(se);
+        expect(sw.findTileToNorth(levelZeroTiles)).toBe(nw);
+        expect(se.findTileToWest(levelZeroTiles)).toBe(sw);
+        expect(se.findTileToNorth(levelZeroTiles)).toBe(ne);
+        expect(nw.findTileToEast(levelZeroTiles)).toBe(ne);
+        expect(nw.findTileToSouth(levelZeroTiles)).toBe(sw);
+        expect(ne.findTileToWest(levelZeroTiles)).toBe(nw);
+        expect(ne.findTileToSouth(levelZeroTiles)).toBe(se);
+    });
+
+    it('can get tiles around a tile when they do not share a common parent', function() {
+        var tilingScheme = new GeographicTilingScheme({
+            numberOfLevelZeroTilesX : 2,
+            numberOfLevelZeroTilesY : 2
+        });
+
+        var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+
+        var northwest = levelZeroTiles[0];
+        var nwse = northwest.southeastChild;
+        var nwne = northwest.northeastChild;
+        var nwsw = northwest.southwestChild;
+
+        var northeast = levelZeroTiles[1];
+        var nesw = northeast.southwestChild;
+        var nenw = northeast.northwestChild;
+        var nese = northeast.southeastChild;
+
+        var southwest = levelZeroTiles[2];
+        var swne = southwest.northeastChild;
+        var swnw = southwest.northwestChild;
+        var swse = southwest.southeastChild;
+
+        var southeast = levelZeroTiles[3];
+        var senw = southeast.northwestChild;
+        var sene = southeast.northeastChild;
+        var sesw = southeast.southwestChild;
+
+        expect(nwse.findTileToEast(levelZeroTiles)).toBe(nesw);
+        expect(nwse.findTileToSouth(levelZeroTiles)).toBe(swne);
+        expect(nwne.findTileToEast(levelZeroTiles)).toBe(nenw);
+        expect(nwsw.findTileToSouth(levelZeroTiles)).toBe(swnw);
+
+        expect(nesw.findTileToWest(levelZeroTiles)).toBe(nwse);
+        expect(nesw.findTileToSouth(levelZeroTiles)).toBe(senw);
+        expect(nenw.findTileToWest(levelZeroTiles)).toBe(nwne);
+        expect(nese.findTileToSouth(levelZeroTiles)).toBe(sene);
+
+        expect(swne.findTileToEast(levelZeroTiles)).toBe(senw);
+        expect(swne.findTileToNorth(levelZeroTiles)).toBe(nwse);
+        expect(swnw.findTileToNorth(levelZeroTiles)).toBe(nwsw);
+        expect(swse.findTileToEast(levelZeroTiles)).toBe(sesw);
+
+        expect(senw.findTileToWest(levelZeroTiles)).toBe(swne);
+        expect(senw.findTileToNorth(levelZeroTiles)).toBe(nesw);
+        expect(sene.findTileToNorth(levelZeroTiles)).toBe(nese);
+        expect(sesw.findTileToWest(levelZeroTiles)).toBe(swse);
+    });
+
+    it('can get adjacent tiles wrapping around the anti-meridian', function() {
+        var tilingScheme = new GeographicTilingScheme({
+            numberOfLevelZeroTilesX : 2,
+            numberOfLevelZeroTilesY : 1
+        });
+
+        var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+
+        var west = levelZeroTiles[0];
+        var wsw = west.southwestChild;
+        var wnw = west.northwestChild;
+
+        var east = levelZeroTiles[1];
+        var ene = east.northeastChild;
+        var ese = east.southeastChild;
+
+        expect(wsw.findTileToWest(levelZeroTiles)).toBe(ese);
+        expect(wnw.findTileToWest(levelZeroTiles)).toBe(ene);
+
+        expect(ene.findTileToEast(levelZeroTiles)).toBe(wnw);
+        expect(ese.findTileToEast(levelZeroTiles)).toBe(wsw);
+    });
+
+    it('returns undefined when asked for adjacent tiles north of the north pole or south of the south pole', function() {
+        var tilingScheme = new GeographicTilingScheme({
+            numberOfLevelZeroTilesX : 2,
+            numberOfLevelZeroTilesY : 1
+        });
+
+        var levelZeroTiles = QuadtreeTile.createLevelZeroTiles(tilingScheme);
+
+        var west = levelZeroTiles[0];
+        var wnw = west.northwestChild;
+        var wsw = west.southwestChild;
+
+        expect(wnw.findTileToNorth(levelZeroTiles)).toBeUndefined();
+        expect(wsw.findTileToSouth(levelZeroTiles)).toBeUndefined();
+    });
+
     describe('createLevelZeroTiles', function() {
         var tilingScheme1x1;
         var tilingScheme2x2;
diff --git a/Specs/Scene/TerrainFillMeshSpec.js b/Specs/Scene/TerrainFillMeshSpec.js
new file mode 100644
index 00000000000..abd987c9efc
--- /dev/null
+++ b/Specs/Scene/TerrainFillMeshSpec.js
@@ -0,0 +1,966 @@
+defineSuite([
+        'Scene/TerrainFillMesh',
+        'Core/Cartesian2',
+        'Core/Cartesian3',
+        'Core/GeographicProjection',
+        'Core/HeightmapTerrainData',
+        'Core/Intersect',
+        'Core/Math',
+        'Scene/Camera',
+        'Scene/GlobeSurfaceTileProvider',
+        'Scene/ImageryLayerCollection',
+        'Scene/QuadtreePrimitive',
+        'Scene/SceneMode',
+        'Scene/TileBoundingRegion',
+        'Scene/TileSelectionResult',
+        '../MockTerrainProvider',
+        '../TerrainTileProcessor'
+    ], function(
+        TerrainFillMesh,
+        Cartesian2,
+        Cartesian3,
+        GeographicProjection,
+        HeightmapTerrainData,
+        Intersect,
+        CesiumMath,
+        Camera,
+        GlobeSurfaceTileProvider,
+        ImageryLayerCollection,
+        QuadtreePrimitive,
+        SceneMode,
+        TileBoundingRegion,
+        TileSelectionResult,
+        MockTerrainProvider,
+        TerrainTileProcessor) {
+    'use strict';
+
+    var processor;
+    var scene;
+    var camera;
+    var frameState;
+    var imageryLayerCollection;
+    var surfaceShaderSet;
+    var mockTerrain;
+    var tileProvider;
+    var quadtree;
+    var rootTiles;
+
+    var center;
+    var west;
+    var south;
+    var east;
+    var north;
+    var southwest;
+    var southeast;
+    var northwest;
+    var northeast;
+
+    beforeEach(function() {
+        scene = {
+            mapProjection: new GeographicProjection(),
+            drawingBufferWidth: 1000,
+            drawingBufferHeight: 1000
+        };
+
+        camera = new Camera(scene);
+
+        frameState = {
+            frameNumber: 0,
+            passes: {
+                render: true
+            },
+            camera: camera,
+            fog: {
+                enabled: false
+            },
+            context: {
+                drawingBufferWidth: scene.drawingBufferWidth,
+                drawingBufferHeight: scene.drawingBufferHeight
+            },
+            mode: SceneMode.SCENE3D,
+            commandList: [],
+            cullingVolume: jasmine.createSpyObj('CullingVolume', ['computeVisibility']),
+            afterRender: []
+        };
+
+        frameState.cullingVolume.computeVisibility.and.returnValue(Intersect.INTERSECTING);
+
+        imageryLayerCollection = new ImageryLayerCollection();
+        surfaceShaderSet = jasmine.createSpyObj('SurfaceShaderSet', ['getShaderProgram']);
+        mockTerrain = new MockTerrainProvider();
+        tileProvider = new GlobeSurfaceTileProvider({
+            terrainProvider: mockTerrain,
+            imageryLayers: imageryLayerCollection,
+            surfaceShaderSet: surfaceShaderSet
+        });
+        quadtree = new QuadtreePrimitive({
+            tileProvider: tileProvider
+        });
+
+        processor = new TerrainTileProcessor(frameState, mockTerrain, imageryLayerCollection);
+        processor.mockWebGL();
+
+        quadtree.render(frameState);
+        rootTiles = quadtree._levelZeroTiles;
+
+        center = rootTiles[0].northeastChild.southwestChild.northeastChild.southwestChild;
+        west = center.findTileToWest(rootTiles);
+        south = center.findTileToSouth(rootTiles);
+        east = center.findTileToEast(rootTiles);
+        north = center.findTileToNorth(rootTiles);
+        southwest = west.findTileToSouth(rootTiles);
+        southeast = east.findTileToSouth(rootTiles);
+        northwest = west.findTileToNorth(rootTiles);
+        northeast = east.findTileToNorth(rootTiles);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                15.0, 16.0, 17.0,
+                22.0, 23.0, 24.0,
+                29.0, 30.0, 31.0
+            ])
+        }), west).createMeshWillSucceed(west);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                31.0, 32.0, 33.0,
+                38.0, 39.0, 40.0,
+                45.0, 46.0, 47.0
+            ])
+        }), south).createMeshWillSucceed(south);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                19.0, 20.0, 21.0,
+                26.0, 27.0, 28.0,
+                33.0, 34.0, 35.0
+            ])
+        }), east).createMeshWillSucceed(east);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                3.0, 4.0, 5.0,
+                10.0, 11.0, 12.0,
+                17.0, 18.0, 19.0
+            ])
+        }), north).createMeshWillSucceed(north);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                29.0, 30.0, 31.0,
+                36.0, 37.0, 38.0,
+                43.0, 44.0, 45.0
+            ])
+        }), southwest).createMeshWillSucceed(southwest);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                33.0, 34.0, 35.0,
+                40.0, 41.0, 42.0,
+                47.0, 48.0, 49.0
+            ])
+        }), southeast).createMeshWillSucceed(southeast);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                1.0, 2.0, 3.0,
+                8.0, 9.0, 10.0,
+                15.0, 16.0, 17.0
+            ])
+        }), northwest).createMeshWillSucceed(northwest);
+
+        mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+            width: 3,
+            height: 3,
+            createdByUpsampling: false,
+            buffer: new Float32Array([
+                5.0, 6.0, 7.0,
+                12.0, 13.0, 14.0,
+                19.0, 20.0, 21.0
+            ])
+        }), northeast).createMeshWillSucceed(northeast);
+    });
+
+    describe('updateFillTiles', function() {
+        it('does nothing if no rendered tiles are provided', function() {
+            expect(function() {
+                TerrainFillMesh.updateFillTiles(tileProvider, [], frameState);
+            }).not.toThrow();
+        });
+
+        it('propagates edges and corners to an unloaded tile', function() {
+            var tiles = [center, west, south, east, north, southwest, southeast, northwest, northeast];
+
+            tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain));
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                var fill = center.data.fill;
+                expect(fill).toBeDefined();
+                expect(fill.westTiles).toEqual([west]);
+                expect(fill.westMeshes).toEqual([west.data.mesh]);
+                expect(fill.southTiles).toEqual([south]);
+                expect(fill.southMeshes).toEqual([south.data.mesh]);
+                expect(fill.eastTiles).toEqual([east]);
+                expect(fill.eastMeshes).toEqual([east.data.mesh]);
+                expect(fill.northTiles).toEqual([north]);
+                expect(fill.northMeshes).toEqual([north.data.mesh]);
+                expect(fill.southwestTile).toEqual(southwest);
+                expect(fill.southwestMesh).toEqual(southwest.data.mesh);
+                expect(fill.southeastTile).toEqual(southeast);
+                expect(fill.southeastMesh).toEqual(southeast.data.mesh);
+                expect(fill.northwestTile).toEqual(northwest);
+                expect(fill.northwestMesh).toEqual(northwest.data.mesh);
+                expect(fill.northeastTile).toEqual(northeast);
+                expect(fill.northeastMesh).toEqual(northeast.data.mesh);
+            });
+        });
+
+        it('propagates fill tile edges to an unloaded tile', function() {
+            var centerSW = center.southwestChild;
+            var centerSE = center.southeastChild;
+            var centerNW = center.northwestChild;
+            var centerNE = center.northeastChild;
+
+            var tiles = [centerSW, centerSE, centerNW, centerNE, west, south, east, north, southwest, southeast, northwest, northeast];
+
+            tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain));
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                var sw = centerSW.data.fill;
+                var se = centerSE.data.fill;
+                var nw = centerNW.data.fill;
+                var ne = centerNE.data.fill;
+
+                expect(sw).toBeDefined();
+                expect(sw.westTiles).toEqual([west]);
+                expect(sw.westMeshes).toEqual([west.data.mesh]);
+                expect(sw.southTiles).toEqual([south]);
+                expect(sw.southMeshes).toEqual([south.data.mesh]);
+                expect(sw.southwestTile).toEqual(southwest);
+                expect(sw.southwestMesh).toEqual(southwest.data.mesh);
+                expect(sw.southeastTile).toBeUndefined();
+                expect(sw.southeastMesh).toBeUndefined();
+                expect(sw.northwestTile).toBeUndefined();
+                expect(sw.northwestMesh).toBeUndefined();
+
+                expect(se).toBeDefined();
+                expect(se.eastTiles).toEqual([east]);
+                expect(se.eastMeshes).toEqual([east.data.mesh]);
+                expect(se.southTiles).toEqual([south]);
+                expect(se.southMeshes).toEqual([south.data.mesh]);
+                expect(se.southeastTile).toEqual(southeast);
+                expect(se.southeastMesh).toEqual(southeast.data.mesh);
+                expect(se.southwestTile).toBeUndefined();
+                expect(se.southwestMesh).toBeUndefined();
+                expect(se.northeastTile).toBeUndefined();
+                expect(se.northeastMesh).toBeUndefined();
+
+                expect(nw).toBeDefined();
+                expect(nw.westTiles).toEqual([west]);
+                expect(nw.westMeshes).toEqual([west.data.mesh]);
+                expect(nw.northTiles).toEqual([north]);
+                expect(nw.northMeshes).toEqual([north.data.mesh]);
+                expect(nw.northwestTile).toEqual(northwest);
+                expect(nw.northwestMesh).toEqual(northwest.data.mesh);
+                expect(nw.southwestTile).toBeUndefined();
+                expect(nw.southwestMesh).toBeUndefined();
+                expect(nw.northeastTile).toBeUndefined();
+                expect(nw.northeastMesh).toBeUndefined();
+
+                expect(ne).toBeDefined();
+                expect(ne.eastTiles).toEqual([east]);
+                expect(ne.eastMeshes).toEqual([east.data.mesh]);
+                expect(ne.northTiles).toEqual([north]);
+                expect(ne.northMeshes).toEqual([north.data.mesh]);
+                expect(ne.northeastTile).toEqual(northeast);
+                expect(ne.northeastMesh).toEqual(northeast.data.mesh);
+                expect(ne.southeastTile).toBeUndefined();
+                expect(ne.southeastMesh).toBeUndefined();
+                expect(ne.northwestTile).toBeUndefined();
+                expect(ne.northwestMesh).toBeUndefined();
+
+                expect(sw.eastTiles[0] === centerSE || se.westTiles[0] === centerSW).toBe(true);
+                expect(nw.eastTiles[0] === centerNE || ne.westTiles[0] === centerNW).toBe(true);
+
+                expect(sw.northTiles[0] === centerNW || nw.southTiles[0] === centerSW).toBe(true);
+                expect(se.northTiles[0] === centerNE || ne.southTiles[0] === centerSE).toBe(true);
+
+                expect(sw.northeastTile === centerNE || ne.southwestTile === centerSW).toBe(true);
+                expect(nw.southeastTile === centerSE || se.northwestTile === centerNW).toBe(true);
+            });
+        });
+
+        it('does not touch disconnected tiles', function() {
+            var disconnected = center.southwestChild.northeastChild;
+
+            var tiles = [disconnected, west, south, east, north, southwest, southeast, northwest, northeast];
+
+            tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain));
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                expect(disconnected.data.fill).toBeUndefined();
+            });
+        });
+
+        it('propagates multiple adjacent source tiles to a destination edge', function() {
+            var tiles = [center, west, south, east, north];
+            [west, south, east, north].forEach(function(tile) {
+                tile.children.forEach(function(child) {
+                    mockTerrain.willBeUnavailable(child);
+                    mockTerrain.upsampleWillSucceed(child);
+                    tiles.push(child);
+                });
+            });
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                var fill = center.data.fill;
+                expect(fill).toBeDefined();
+                expect(fill.westTiles).toEqual([west.northeastChild, west.southeastChild]);
+                expect(fill.southTiles).toEqual([south.northwestChild, south.northeastChild]);
+                expect(fill.eastTiles).toEqual([east.southwestChild, east.northwestChild]);
+                expect(fill.northTiles).toEqual([north.southeastChild, north.southwestChild]);
+            });
+        });
+
+        it('adjusts existing fill tiles when adjacent tiles are loaded', function() {
+            var tiles = [center, west, south, north];
+
+            tiles.forEach(mockTerrain.createMeshWillSucceed.bind(mockTerrain));
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                var fill = center.data.fill;
+                expect(fill).toBeDefined();
+                expect(fill.westTiles).toEqual([west]);
+                expect(fill.westMeshes).toEqual([west.data.mesh]);
+                expect(fill.southTiles).toEqual([south]);
+                expect(fill.southMeshes).toEqual([south.data.mesh]);
+                expect(fill.eastTiles).toEqual([]);
+                expect(fill.eastMeshes).toEqual([]);
+                expect(fill.northTiles).toEqual([north]);
+                expect(fill.northMeshes).toEqual([north.data.mesh]);
+
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 8);
+
+                tiles.push(east);
+
+                return processor.process(tiles);
+            }).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+
+                var fill = center.data.fill;
+                expect(fill).toBeDefined();
+                expect(fill.westTiles).toEqual([west]);
+                expect(fill.westMeshes).toEqual([west.data.mesh]);
+                expect(fill.southTiles).toEqual([south]);
+                expect(fill.southMeshes).toEqual([south.data.mesh]);
+                expect(fill.eastTiles).toEqual([east]);
+                expect(fill.eastMeshes).toEqual([east.data.mesh]);
+                expect(fill.northTiles).toEqual([north]);
+                expect(fill.northMeshes).toEqual([north.data.mesh]);
+
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 9);
+                expectVertex(fill, 1.0, 0.5, 26.0);
+            });
+        });
+
+        it('adjusts existing fill tiles when an adjacent fill tile changes', function() {
+            var dontLoad = [east, south, southeast];
+            dontLoad.forEach(mockTerrain.requestTileGeometryWillDefer.bind(mockTerrain));
+
+            var tiles = [center, west, south, east, north, southwest, southeast, northwest, northeast];
+
+            return processor.process(tiles).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+                center.data.fill.update(tileProvider, frameState);
+                south.data.fill.update(tileProvider, frameState);
+                east.data.fill.update(tileProvider, frameState);
+                southeast.data.fill.update(tileProvider, frameState);
+
+                expectVertexCount(center.data.fill, 7);
+                expectVertex(center.data.fill, 0.0, 0.0, 31.0);
+                expectVertex(center.data.fill, 0.0, 0.5, 24.0);
+                expectVertex(center.data.fill, 0.0, 1.0, 17.0);
+                expectVertex(center.data.fill, 0.5, 1.0, 18.0);
+                expectVertex(center.data.fill, 1.0, 1.0, 19.0);
+
+                expectVertexCount(south.data.fill, 6);
+                expectVertex(south.data.fill, 0.0, 1.0, 31.0);
+                expectVertex(south.data.fill, 0.0, 0.5, 38.0);
+                expectVertex(south.data.fill, 0.0, 0.0, 45.0);
+
+                expectVertexCount(east.data.fill, 6);
+                expectVertex(east.data.fill, 0.0, 1.0, 19.0);
+                expectVertex(east.data.fill, 0.5, 1.0, 20.0);
+                expectVertex(east.data.fill, 1.0, 1.0, 21.0);
+
+                expectVertexCount(southeast.data.fill, 5);
+
+                expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 0.0, 1.0));
+                expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(south.data.fill, 1.0, 1.0));
+                expect(getHeight(center.data.fill, 1.0, 0.0)).toBe(getHeight(east.data.fill, 0.0, 0.0));
+                expect(getHeight(center.data.fill, 1.0, 1.0)).toBe(getHeight(east.data.fill, 0.0, 1.0));
+                expect(getHeight(east.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 1.0, 1.0));
+                expect(getHeight(south.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 0.0, 0.0));
+
+                // Now load the south tile.
+                mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                    width: 3,
+                    height: 3,
+                    createdByUpsampling: false,
+                    buffer: new Float32Array([
+                        31.0, 32.0, 33.0,
+                        38.0, 39.0, 40.0,
+                        45.0, 46.0, 47.0
+                    ])
+                }), south).createMeshWillSucceed(south);
+
+                return processor.process(tiles);
+            }).then(function() {
+                tiles.forEach(markRendered);
+
+                TerrainFillMesh.updateFillTiles(tileProvider, tiles, frameState);
+                center.data.fill.update(tileProvider, frameState);
+                east.data.fill.update(tileProvider, frameState);
+                southeast.data.fill.update(tileProvider, frameState);
+
+                expect(south.data.fill).toBeUndefined();
+
+                expectVertexCount(center.data.fill, 8);
+                expectVertex(center.data.fill, 0.0, 0.0, 31.0);
+                expectVertex(center.data.fill, 0.0, 0.5, 24.0);
+                expectVertex(center.data.fill, 0.0, 1.0, 17.0);
+                expectVertex(center.data.fill, 0.5, 1.0, 18.0);
+                expectVertex(center.data.fill, 1.0, 1.0, 19.0);
+                expectVertex(center.data.fill, 1.0, 0.0, 33.0);
+
+                expectVertexCount(east.data.fill, 6);
+                expectVertex(east.data.fill, 0.0, 1.0, 19.0);
+                expectVertex(east.data.fill, 0.5, 1.0, 20.0);
+                expectVertex(east.data.fill, 1.0, 1.0, 21.0);
+                expectVertex(east.data.fill, 0.0, 0.0, 33.0);
+
+                expectVertexCount(southeast.data.fill, 6);
+                expectVertex(southeast.data.fill, 0.0, 0.0, 47.0);
+                expectVertex(southeast.data.fill, 0.0, 0.5, 40.0);
+                expectVertex(southeast.data.fill, 0.0, 1.0, 33.0);
+
+                expect(getHeight(east.data.fill, 1.0, 0.0)).toBe(getHeight(southeast.data.fill, 1.0, 1.0));
+            });
+        });
+
+        // Mark all the tiles rendered.
+        function markRendered(tile) {
+            quadtree._lastSelectionFrameNumber = frameState.frameNumber;
+            tile._lastSelectionResultFrame = frameState.frameNumber;
+            tile._lastSelectionResult = TileSelectionResult.RENDERED;
+
+            var parent = tile.parent;
+            while (parent) {
+                if (parent._lastSelectionResultFrame !== frameState.frameNumber) {
+                    parent._lastSelectionResultFrame = frameState.frameNumber;
+                    parent._lastSelectionResult = TileSelectionResult.REFINED;
+                }
+                parent = parent.parent;
+            }
+        }
+    });
+
+    describe('update', function() {
+        it('puts a middle height at the four corners and center when there are no adjacent tiles', function() {
+            return processor.process([center]).then(function() {
+                center.data.tileBoundingRegion = new TileBoundingRegion({
+                    rectangle: center.rectangle,
+                    minimumHeight: 1.0,
+                    maximumHeight: 3.0,
+                    computeBoundingVolumes: false
+                });
+
+                var fill = center.data.fill = new TerrainFillMesh(center);
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 5);
+                expectVertex(fill, 0.0, 0.0, 2.0);
+                expectVertex(fill, 0.0, 1.0, 2.0);
+                expectVertex(fill, 1.0, 0.0, 2.0);
+                expectVertex(fill, 1.0, 1.0, 2.0);
+                expectVertex(fill, 0.5, 0.5, 2.0);
+            });
+        });
+
+        it('puts zero height at the four corners and center when there are no adjacent tiles and no bounding region', function() {
+            return processor.process([center]).then(function() {
+                var fill = center.data.fill = new TerrainFillMesh(center);
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 5);
+                expectVertex(fill, 0.0, 0.0, 0.0);
+                expectVertex(fill, 0.0, 1.0, 0.0);
+                expectVertex(fill, 1.0, 0.0, 0.0);
+                expectVertex(fill, 1.0, 1.0, 0.0);
+                expectVertex(fill, 0.5, 0.5, 0.0);
+            });
+        });
+
+        it('uses adjacent edge heights', function() {
+            return processor.process([center, west, south, east, north]).then(function() {
+                var fill = center.data.fill = new TerrainFillMesh(center);
+
+                fill.westTiles.push(west);
+                fill.westMeshes.push(west.data.mesh);
+                fill.southTiles.push(south);
+                fill.southMeshes.push(south.data.mesh);
+                fill.eastTiles.push(east);
+                fill.eastMeshes.push(east.data.mesh);
+                fill.northTiles.push(north);
+                fill.northMeshes.push(north.data.mesh);
+
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 9);
+                expectVertex(fill, 0.0, 0.0, 31.0);
+                expectVertex(fill, 0.5, 0.0, 32.0);
+                expectVertex(fill, 1.0, 0.0, 33.0);
+                expectVertex(fill, 0.0, 0.5, 24.0);
+                expectVertex(fill, 0.5, 0.5, (33.0 + 17.0) / 2);
+                expectVertex(fill, 1.0, 0.5, 26.0);
+                expectVertex(fill, 0.0, 1.0, 17.0);
+                expectVertex(fill, 0.5, 1.0, 18.0);
+                expectVertex(fill, 1.0, 1.0, 19.0);
+            });
+        });
+
+        it('uses adjacent corner heights if adjacent edges are not available', function() {
+            return processor.process([center, southwest, southeast, northwest, northeast]).then(function() {
+                var fill = center.data.fill = new TerrainFillMesh(center);
+
+                fill.southwestTile = southwest;
+                fill.southwestMesh = southwest.data.mesh;
+                fill.southeastTile = southeast;
+                fill.southeastMesh = southeast.data.mesh;
+                fill.northwestTile = northwest;
+                fill.northwestMesh = northwest.data.mesh;
+                fill.northeastTile = northeast;
+                fill.northeastMesh = northeast.data.mesh;
+
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 5);
+                expectVertex(fill, 0.0, 0.0, 31.0);
+                expectVertex(fill, 1.0, 0.0, 33.0);
+                expectVertex(fill, 0.0, 1.0, 17.0);
+                expectVertex(fill, 1.0, 1.0, 19.0);
+                expectVertex(fill, 0.5, 0.5, (17.0 + 33.0) / 2.0);
+            });
+        });
+
+        it('finds a suitable corner vertex in a less detailed tile', function() {
+            var sw = center.southwestChild;
+            var se = center.southeastChild;
+            var nw = center.northwestChild;
+            var ne = center.northeastChild;
+
+            return processor.process([sw, se, nw, ne, west, south, east, north]).then(function() {
+                var fillSW = sw.data.fill = new TerrainFillMesh(sw);
+                var fillSE = se.data.fill = new TerrainFillMesh(se);
+                var fillNW = nw.data.fill = new TerrainFillMesh(nw);
+                var fillNE = ne.data.fill = new TerrainFillMesh(ne);
+
+                fillSW.westTiles.push(west);
+                fillSW.westMeshes.push(west.data.mesh);
+                fillSW.southTiles.push(south);
+                fillSW.southMeshes.push(south.data.mesh);
+
+                fillSE.eastTiles.push(east);
+                fillSE.eastMeshes.push(east.data.mesh);
+                fillSE.southTiles.push(south);
+                fillSE.southMeshes.push(south.data.mesh);
+
+                fillNW.westTiles.push(west);
+                fillNW.westMeshes.push(west.data.mesh);
+                fillNW.northTiles.push(north);
+                fillNW.northMeshes.push(north.data.mesh);
+
+                fillNE.eastTiles.push(east);
+                fillNE.eastMeshes.push(east.data.mesh);
+                fillNE.northTiles.push(north);
+                fillNE.northMeshes.push(north.data.mesh);
+
+                fillSW.update(tileProvider, frameState);
+                fillSE.update(tileProvider, frameState);
+                fillNW.update(tileProvider, frameState);
+                fillNE.update(tileProvider, frameState);
+
+                expectVertexCount(fillSW, 5);
+                expectVertex(fillSW, 0.0, 0.0, 31.0);
+                expectVertex(fillSW, 1.0, 0.0, 32.0);
+                expectVertex(fillSW, 0.0, 1.0, 24.0);
+                expectVertex(fillSW, 1.0, 1.0, (24.0 + 32.0) / 2);
+                expectVertex(fillSW, 0.5, 0.5, (24.0 + 32.0) / 2);
+
+                expectVertexCount(fillSE, 5);
+                expectVertex(fillSE, 0.0, 0.0, 32.0);
+                expectVertex(fillSE, 1.0, 0.0, 33.0);
+                expectVertex(fillSE, 0.0, 1.0, (32.0 + 26.0) / 2);
+                expectVertex(fillSE, 1.0, 1.0, 26.0);
+                expectVertex(fillSE, 0.5, 0.5, (26.0 + 33.0) / 2);
+
+                expectVertexCount(fillNW, 5);
+                expectVertex(fillNW, 0.0, 0.0, 24.0);
+                expectVertex(fillNW, 1.0, 0.0, (18.0 + 24.0) / 2);
+                expectVertex(fillNW, 0.0, 1.0, 17.0);
+                expectVertex(fillNW, 1.0, 1.0, 18.0);
+                expectVertex(fillNW, 0.5, 0.5, (17.0 + 24.0) / 2);
+
+                expectVertexCount(fillNE, 5);
+                expectVertex(fillNE, 0.0, 0.0, (18.0 + 26.0) / 2);
+                expectVertex(fillNE, 1.0, 0.0, 26.0);
+                expectVertex(fillNE, 0.0, 1.0, 18.0);
+                expectVertex(fillNE, 1.0, 1.0, 19.0);
+                expectVertex(fillNE, 0.5, 0.5, (18.0 + 26.0) / 2);
+            });
+        });
+
+        it('interpolates a suitable corner vertex from a less detailed tile', function() {
+            var sw = center.southwestChild.southwestChild;
+            var se = center.southeastChild.southeastChild;
+            var nw = center.northwestChild.northwestChild;
+            var ne = center.northeastChild.northeastChild;
+
+            return processor.process([sw, se, nw, ne, west, south, east, north]).then(function() {
+                var fillSW = sw.data.fill = new TerrainFillMesh(sw);
+                var fillSE = se.data.fill = new TerrainFillMesh(se);
+                var fillNW = nw.data.fill = new TerrainFillMesh(nw);
+                var fillNE = ne.data.fill = new TerrainFillMesh(ne);
+
+                fillSW.westTiles.push(west);
+                fillSW.westMeshes.push(west.data.mesh);
+                fillSW.southTiles.push(south);
+                fillSW.southMeshes.push(south.data.mesh);
+
+                fillSE.eastTiles.push(east);
+                fillSE.eastMeshes.push(east.data.mesh);
+                fillSE.southTiles.push(south);
+                fillSE.southMeshes.push(south.data.mesh);
+
+                fillNW.westTiles.push(west);
+                fillNW.westMeshes.push(west.data.mesh);
+                fillNW.northTiles.push(north);
+                fillNW.northMeshes.push(north.data.mesh);
+
+                fillNE.eastTiles.push(east);
+                fillNE.eastMeshes.push(east.data.mesh);
+                fillNE.northTiles.push(north);
+                fillNE.northMeshes.push(north.data.mesh);
+
+                fillSW.update(tileProvider, frameState);
+                fillSE.update(tileProvider, frameState);
+                fillNW.update(tileProvider, frameState);
+                fillNE.update(tileProvider, frameState);
+
+                expectVertexCount(fillSW, 5);
+                expectVertex(fillSW, 0.0, 0.0, 31.0);
+                expectVertex(fillSW, 1.0, 0.0, (31.0 + 32.0) / 2);
+                expectVertex(fillSW, 0.0, 1.0, (31.0 + 24.0) / 2);
+                expectVertex(fillSW, 1.0, 1.0, ((31.0 + 32.0) / 2 + (31.0 + 24.0) / 2) / 2);
+                expectVertex(fillSW, 0.5, 0.5, ((31.0 + 32.0) / 2 + (31.0 + 24.0) / 2) / 2);
+
+                expectVertexCount(fillSE, 5);
+                expectVertex(fillSE, 0.0, 0.0, (32.0 + 33.0) / 2);
+                expectVertex(fillSE, 1.0, 0.0, 33.0);
+                expectVertex(fillSE, 0.0, 1.0, ((32.0 + 33.0) / 2 + (33.0 + 26.0) / 2) / 2);
+                expectVertex(fillSE, 1.0, 1.0, (33.0 + 26.0) / 2);
+                expectVertex(fillSE, 0.5, 0.5, (33.0 + (33.0 + 26.0) / 2) / 2);
+
+                expectVertexCount(fillNW, 5);
+                expectVertex(fillNW, 0.0, 0.0, (17.0 + 24.0) / 2);
+                expectVertex(fillNW, 1.0, 0.0, ((17.0 + 18.0) / 2 + (17.0 + 24.0) / 2) / 2);
+                expectVertex(fillNW, 0.0, 1.0, 17.0);
+                expectVertex(fillNW, 1.0, 1.0, (17.0 + 18.0) / 2);
+                expectVertex(fillNW, 0.5, 0.5, (17.0 + (17.0 + 24.0) / 2) / 2);
+
+                expectVertexCount(fillNE, 5);
+                expectVertex(fillNE, 0.0, 0.0, ((19.0 + 26.0) / 2 + (18.0 + 19.0) / 2) / 2);
+                expectVertex(fillNE, 1.0, 0.0, (19.0 + 26.0) / 2);
+                expectVertex(fillNE, 0.0, 1.0, (18.0 + 19.0) / 2);
+                expectVertex(fillNE, 1.0, 1.0, 19.0);
+                expectVertex(fillNE, 0.5, 0.5, ((18.0 + 19.0) / 2 + (19.0 + 26.0) / 2) / 2);
+            });
+        });
+
+        it('uses the height of the closest vertex when an edge does not include the corner', function() {
+            var westN = west.northeastChild.southeastChild;
+            var westS = west.southeastChild.northeastChild;
+            var eastN = east.northwestChild.southwestChild;
+            var eastS = east.southwestChild.northwestChild;
+            var northW = north.southwestChild.southeastChild;
+            var northE = north.southeastChild.southwestChild;
+            var southW = south.northwestChild.northeastChild;
+            var southE = south.northeastChild.northwestChild;
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    1.0, 1.0,
+                    1.5, 1.5
+                ])
+            }), westN).createMeshWillSucceed(westN);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    1.5, 1.5,
+                    2.0, 2.0
+                ])
+            }), westS).createMeshWillSucceed(westS);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    3.0, 3.0,
+                    3.5, 3.5
+                ])
+            }), eastN).createMeshWillSucceed(eastN);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    3.5, 3.5,
+                    4.0, 4.0
+                ])
+            }), eastS).createMeshWillSucceed(eastS);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    5.0, 5.5,
+                    5.0, 5.5
+                ])
+            }), northW).createMeshWillSucceed(northW);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    5.5, 6.0,
+                    6.5, 6.0
+                ])
+            }), northE).createMeshWillSucceed(northE);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    7.0, 7.5,
+                    7.0, 7.5
+                ])
+            }), southW).createMeshWillSucceed(southW);
+
+            mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                width: 2,
+                height: 2,
+                createdByUpsampling: false,
+                buffer: new Float32Array([
+                    7.5, 8.0,
+                    7.5, 8.0
+                ])
+            }), southE).createMeshWillSucceed(southE);
+
+            return processor.process([center, westN, westS, eastN, eastS, northE, northW, southE, southW]).then(function() {
+                var fill = center.data.fill = new TerrainFillMesh(center);
+
+                fill.westTiles.push(westN, westS);
+                fill.westMeshes.push(westN.data.mesh, westS.data.mesh);
+                fill.eastTiles.push(eastS, eastN);
+                fill.eastMeshes.push(eastS.data.mesh, eastN.data.mesh);
+                fill.northTiles.push(northE, northW);
+                fill.northMeshes.push(northE.data.mesh, northW.data.mesh);
+                fill.southTiles.push(southW, southE);
+                fill.southMeshes.push(southW.data.mesh, southE.data.mesh);
+
+                fill.update(tileProvider, frameState);
+
+                expectVertexCount(fill, 17);
+                expectVertex(fill, 0.0, 0.0, (2.0 + 7.0) / 2);
+                expectVertex(fill, 0.0, 0.25, 2.0);
+                expectVertex(fill, 0.0, 0.5, 1.5);
+                expectVertex(fill, 0.0, 0.75, 1.0);
+                expectVertex(fill, 0.0, 1.0, (1.0 + 5.0) / 2);
+                expectVertex(fill, 1.0, 0.0, (4.0 + 8.0) / 2);
+                expectVertex(fill, 1.0, 0.25, 4.0);
+                expectVertex(fill, 1.0, 0.5, 3.5);
+                expectVertex(fill, 1.0, 0.75, 3.0);
+                expectVertex(fill, 1.0, 1.0, (3.0 + 6.0) / 2);
+            });
+        });
+
+        describe('correctly transforms texture coordinates across the anti-meridian', function() {
+            var westernHemisphere;
+            var easternHemisphere;
+
+            beforeEach(function() {
+                westernHemisphere = rootTiles[0].southwestChild.northwestChild.southwestChild.northwestChild;
+                easternHemisphere = rootTiles[1].southeastChild.northeastChild.southeastChild.northeastChild;
+
+                // Make sure we have a standard geographic tiling scheme with two root tiles,
+                // the first covering the western hemisphere and the second the eastern.
+                expect(rootTiles.length).toBe(2);
+                expect(westernHemisphere.x).toBe(0);
+                expect(easternHemisphere.x).toBe(31);
+            });
+
+            it('western hemisphere to eastern hemisphere', function() {
+                mockTerrain.requestTileGeometryWillDefer(easternHemisphere);
+                mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                    width: 3,
+                    height: 3,
+                    createdByUpsampling: false,
+                    buffer: new Float32Array([
+                        1.0, 2.0, 3.0,
+                        4.0, 5.0, 6.0,
+                        7.0, 8.0, 9.0
+                    ])
+                }), westernHemisphere).createMeshWillSucceed(westernHemisphere);
+
+                return processor.process([westernHemisphere, easternHemisphere]).then(function() {
+                    var fill = easternHemisphere.data.fill = new TerrainFillMesh(easternHemisphere);
+
+                    fill.eastTiles.push(westernHemisphere);
+                    fill.eastMeshes.push(westernHemisphere.data.mesh);
+
+                    fill.update(tileProvider, frameState);
+
+                    expectVertexCount(fill, 6);
+                    expectVertex(fill, 1.0, 0.0, 7.0);
+                    expectVertex(fill, 1.0, 0.5, 4.0);
+                    expectVertex(fill, 1.0, 1.0, 1.0);
+                });
+            });
+
+            it('eastern hemisphere to western hemisphere', function() {
+                mockTerrain.requestTileGeometryWillSucceedWith(new HeightmapTerrainData({
+                    width: 3,
+                    height: 3,
+                    createdByUpsampling: false,
+                    buffer: new Float32Array([
+                        10.0, 11.0, 12.0,
+                        13.0, 14.0, 15.0,
+                        16.0, 17.0, 18.0
+                    ])
+                }), easternHemisphere).createMeshWillSucceed(easternHemisphere);
+                mockTerrain.requestTileGeometryWillDefer(westernHemisphere);
+
+                return processor.process([westernHemisphere, easternHemisphere]).then(function() {
+                    var fill = westernHemisphere.data.fill = new TerrainFillMesh(westernHemisphere);
+
+                    fill.westTiles.push(easternHemisphere);
+                    fill.westMeshes.push(easternHemisphere.data.mesh);
+
+                    fill.update(tileProvider, frameState);
+
+                    expectVertexCount(fill, 6);
+                    expectVertex(fill, 0.0, 0.0, 18.0);
+                    expectVertex(fill, 0.0, 0.5, 15.0);
+                    expectVertex(fill, 0.0, 1.0, 12.0);
+                });
+            });
+        });
+    });
+
+    var textureCoordinateScratch = new Cartesian2();
+    var positionScratch = new Cartesian3();
+    var expectedPositionScratch = new Cartesian3();
+
+    function getHeight(fill, u, v) {
+        var mesh = fill.mesh;
+        var rectangle = fill.tile.rectangle;
+        var encoding = mesh.encoding;
+        var vertices = mesh.vertices;
+        var stride = encoding.getStride();
+        var count = mesh.vertices.length / stride;
+
+        for (var i = 0; i < count; ++i) {
+            var tc = encoding.decodeTextureCoordinates(vertices, i, textureCoordinateScratch);
+            var vertexHeight = encoding.decodeHeight(vertices, i);
+            var vertexPosition = encoding.decodePosition(vertices, i, positionScratch);
+            if (Math.abs(u - tc.x) < 1e-5 && Math.abs(v - tc.y) < CesiumMath.EPSILON5) {
+                var longitude = CesiumMath.lerp(rectangle.west, rectangle.east, u);
+                var latitude = CesiumMath.lerp(rectangle.south, rectangle.north, v);
+                var expectedPosition = Cartesian3.fromRadians(longitude, latitude, vertexHeight, undefined, expectedPositionScratch);
+                expect(vertexPosition).toEqualEpsilon(expectedPosition, 1);
+                return vertexHeight;
+            }
+        }
+
+        fail('Vertex with u=' + u + ', v=' + v + ' does not exist.');
+    }
+
+    function expectVertex(fill, u, v, height) {
+        var vertexHeight = getHeight(fill, u, v);
+        expect(vertexHeight).toEqualEpsilon(height, CesiumMath.EPSILON5);
+    }
+
+    function expectVertexCount(fill, count) {
+        expect(fill.mesh.vertices.length).toBe(count * fill.mesh.encoding.getStride());
+    }
+});
diff --git a/Specs/TerrainTileProcessor.js b/Specs/TerrainTileProcessor.js
new file mode 100644
index 00000000000..97574b03fa7
--- /dev/null
+++ b/Specs/TerrainTileProcessor.js
@@ -0,0 +1,123 @@
+define([
+    'Core/clone',
+    'Renderer/Texture',
+    'Scene/GlobeSurfaceTile',
+    'Scene/ImageryLayer',
+    'Scene/TerrainState',
+    'ThirdParty/when'
+], function(
+    clone,
+    Texture,
+    GlobeSurfaceTile,
+    ImageryLayer,
+    TerrainState,
+    when) {
+    'use strict';
+
+    function TerrainTileProcessor(frameState, terrainProvider, imageryLayerCollection) {
+        this.frameState = frameState;
+        this.terrainProvider = terrainProvider;
+        this.imageryLayerCollection = imageryLayerCollection;
+    }
+
+    // Processes the given list of tiles until all terrain and imagery states stop changing.
+    TerrainTileProcessor.prototype.process = function(tiles, maxIterations) {
+        var that = this;
+
+        var deferred = when.defer();
+
+        function getState(tile) {
+            return [
+                tile.state,
+                tile.data ? tile.data.terrainState : undefined,
+                tile.data && tile.data.imagery ? tile.data.imagery.map(function(imagery) {
+                    return [
+                        imagery.readyImagery ? imagery.readyImagery.state : undefined,
+                        imagery.loadingImagery ? imagery.loadingImagery.state : undefined
+                    ];
+                }) : []
+            ];
+        }
+
+        function statesAreSame(a, b) {
+            if (a.length !== b.length) {
+                return false;
+            }
+
+            var same = true;
+            for (var i = 0; i < a.length; ++i) {
+                if (Array.isArray(a[i]) && Array.isArray(b[i])) {
+                    same = same && statesAreSame(a[i], b[i]);
+                } else if (Array.isArray(a[i]) || Array.isArray(b[i])) {
+                    same = false;
+                } else {
+                    same = same && a[i] === b[i];
+                }
+            }
+
+            return same;
+        }
+
+        var iterations = 0;
+
+        function next() {
+            ++iterations;
+            ++that.frameState.frameNumber;
+
+            // Keep going until all terrain and imagery provider are ready and states are no longer changing.
+            var changed = !that.terrainProvider.ready;
+
+            for (var i = 0; i < that.imageryLayerCollection.length; ++i) {
+                changed = changed || !that.imageryLayerCollection.get(i).imageryProvider.ready;
+            }
+
+            if (that.terrainProvider.ready) {
+                tiles.forEach(function(tile) {
+                    var beforeState = getState(tile);
+                    GlobeSurfaceTile.processStateMachine(tile, that.frameState, that.terrainProvider, that.imageryLayerCollection);
+                    var afterState = getState(tile);
+                    changed =
+                        changed ||
+                        tile.data.terrainState === TerrainState.RECEIVING ||
+                        tile.data.terrainState === TerrainState.TRANSFORMING ||
+                        !statesAreSame(beforeState, afterState);
+                });
+            }
+
+            if (!changed || iterations >= maxIterations) {
+                deferred.resolve(iterations);
+            } else {
+                setTimeout(next, 0);
+            }
+        }
+
+        next();
+
+        return deferred.promise;
+    };
+
+    TerrainTileProcessor.prototype.mockWebGL = function() {
+        spyOn(GlobeSurfaceTile, '_createVertexArrayForMesh').and.callFake(function() {
+            var vertexArray = jasmine.createSpyObj('VertexArray', ['destroy']);
+            return vertexArray;
+        });
+
+        spyOn(ImageryLayer.prototype, '_createTextureWebGL').and.callFake(function(context, imagery) {
+            var texture = jasmine.createSpyObj('Texture', ['destroy']);
+            texture.width = imagery.image.width;
+            texture.height = imagery.image.height;
+            return texture;
+        });
+
+        spyOn(ImageryLayer.prototype, '_finalizeReprojectTexture');
+
+        spyOn(Texture, 'create').and.callFake(function(options) {
+            var result = clone(options);
+            result.destroy = function() {};
+            return result;
+        });
+    };
+
+    return TerrainTileProcessor;
+});
+
diff --git a/Specs/createGlobe.js b/Specs/createGlobe.js
index f55778f9d7c..b0d40c30e77 100644
--- a/Specs/createGlobe.js
+++ b/Specs/createGlobe.js
@@ -25,7 +25,6 @@ define([
                 return 0.0;
             },
             _surface : {},
-            tileLoadedEvent : new Event(),
             imageryLayersUpdatedEvent : new Event(),
             _terrainProvider : undefined,
             terrainProviderChanged : new Event(),
diff --git a/Specs/createTileKey.js b/Specs/createTileKey.js
new file mode 100644
index 00000000000..a8892f96b40
--- /dev/null
+++ b/Specs/createTileKey.js
@@ -0,0 +1,24 @@
+define([
+    'Core/defined',
+    'Core/DeveloperError'
+], function(
+    defined,
+    DeveloperError) {
+   'use strict';
+
+    function createTileKey(xOrTile, y, level) {
+        if (!defined(xOrTile)) {
+            throw new DeveloperError('xOrTile is required');
+        }
+
+        if (typeof xOrTile === 'object') {
+            var tile = xOrTile;
+            xOrTile = tile.x;
+            y = tile.y;
+            level = tile.level;
+        }
+        return 'L' + level + 'X' + xOrTile + 'Y' + y;
+    }
+
+    return createTileKey;
+});
diff --git a/Specs/runLater.js b/Specs/runLater.js
new file mode 100644
index 00000000000..54aa36ec05a
--- /dev/null
+++ b/Specs/runLater.js
@@ -0,0 +1,24 @@
+define([
+    'Core/defaultValue',
+    'ThirdParty/when'
+], function(
+    defaultValue,
+    when) {
+    'use strict';
+
+    function runLater(functionToRunLater, milliseconds) {
+        milliseconds = defaultValue(milliseconds, 0);
+
+        var deferred = when.defer();
+        setTimeout(function() {
+            try {
+                deferred.resolve(functionToRunLater());
+            } catch (e) {
+                deferred.reject(e);
+            }
+        }, milliseconds);
+        return deferred.promise;
+    }
+
+     return runLater;
+ });