diff --git a/debug/pathological-flyto.html b/debug/pathological-flyto.html new file mode 100644 index 00000000000..bc1c5d3c7c0 --- /dev/null +++ b/debug/pathological-flyto.html @@ -0,0 +1,201 @@ + + + + + + + Mapbox GL JS Pathological FlyTo + + + + + + + +
+ + + + + +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/render/draw_fill.js b/src/render/draw_fill.js index f495de34912..aa8020c27e3 100644 --- a/src/render/draw_fill.js +++ b/src/render/draw_fill.js @@ -89,7 +89,9 @@ function drawFillTiles(painter: Painter, sourceCache: SourceCache, layer: FillSt if (image) { painter.context.activeTexture.set(gl.TEXTURE0); - tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + if (tile.imageAtlasTexture) { + tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } programConfiguration.updatePaintBuffers(); } diff --git a/src/render/draw_fill_extrusion.js b/src/render/draw_fill_extrusion.js index a8d30e7a4c5..2465a84e957 100644 --- a/src/render/draw_fill_extrusion.js +++ b/src/render/draw_fill_extrusion.js @@ -340,7 +340,9 @@ function drawExtrusionTiles(painter: Painter, source: SourceCache, layer: FillEx if (image) { painter.context.activeTexture.set(gl.TEXTURE0); - tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + if (tile.imageAtlasTexture) { + tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } programConfiguration.updatePaintBuffers(); } const constantPattern = patternProperty.constantOr(null); diff --git a/src/render/draw_line.js b/src/render/draw_line.js index 0ab0a215653..9a88f1f1d65 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -139,12 +139,16 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay } if (dasharray) { context.activeTexture.set(gl.TEXTURE0); - tile.lineAtlasTexture.bind(gl.LINEAR, gl.REPEAT); + if (tile.lineAtlasTexture) { + tile.lineAtlasTexture.bind(gl.LINEAR, gl.REPEAT); + } programConfiguration.updatePaintBuffers(); } if (image) { context.activeTexture.set(gl.TEXTURE0); - tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + if (tile.imageAtlasTexture) { + tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } programConfiguration.updatePaintBuffers(); } diff --git a/src/render/draw_raster.js b/src/render/draw_raster.js index 822ca505095..be01412ea62 100644 --- a/src/render/draw_raster.js +++ b/src/render/draw_raster.js @@ -149,22 +149,26 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty let parentScaleBy, parentTL; context.activeTexture.set(gl.TEXTURE0); - tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE); + if (tile.texture) { + tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE); + } context.activeTexture.set(gl.TEXTURE1); if (parentTile) { - parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE); + if (parentTile.texture) { + parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE); + } parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ); parentTL = [tile.tileID.canonical.x * parentScaleBy % 1, tile.tileID.canonical.y * parentScaleBy % 1]; - } else { + } else if (tile.texture) { tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE); } // Enable trilinear filtering on tiles only beyond 20 degrees pitch, // to prevent it from compromising image crispness on flat or low tilted maps. - if (tile.texture.useMipmap && context.extTextureFilterAnisotropic && painter.transform.pitch > 20) { + if (tile.texture && tile.texture.useMipmap && context.extTextureFilterAnisotropic && painter.transform.pitch > 20) { gl.texParameterf(gl.TEXTURE_2D, context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, context.extTextureFilterAnisotropicMax); } diff --git a/src/render/draw_symbol.js b/src/render/draw_symbol.js index cc2f81a5b40..5ba09143160 100644 --- a/src/render/draw_symbol.js +++ b/src/render/draw_symbol.js @@ -33,7 +33,7 @@ import type Painter from './painter.js'; import type SourceCache from '../source/source_cache.js'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer.js'; import type SymbolBucket, {SymbolBuffers} from '../data/bucket/symbol_bucket.js'; -import type Texture from '../render/texture.js'; +import Texture from '../render/texture.js'; import type ColorMode from '../gl/color_mode.js'; import {OverscaledTileID} from '../source/tile_id.js'; import type {UniformValues} from './uniform_binding.js'; @@ -49,7 +49,7 @@ type SymbolTileRenderState = { program: any, buffers: SymbolBuffers, uniformValues: any, - atlasTexture: Texture, + atlasTexture: Texture | null, atlasTextureIcon: Texture | null, atlasInterpolation: any, atlasInterpolationIcon: any, @@ -327,27 +327,27 @@ function drawLayerSymbols(painter: Painter, sourceCache: SourceCache, layer: Sym let texSize: [number, number]; let texSizeIcon: [number, number] = [0, 0]; - let atlasTexture; + let atlasTexture: Texture | null; let atlasInterpolation; - let atlasTextureIcon = null; + let atlasTextureIcon: Texture | null = null; let atlasInterpolationIcon; if (isText) { - atlasTexture = tile.glyphAtlasTexture; + atlasTexture = tile.glyphAtlasTexture ? tile.glyphAtlasTexture : null; atlasInterpolation = gl.LINEAR; - texSize = tile.glyphAtlasTexture.size; + texSize = tile.glyphAtlasTexture ? tile.glyphAtlasTexture.size : [0, 0]; if (bucket.iconsInText) { - texSizeIcon = tile.imageAtlasTexture.size; - atlasTextureIcon = tile.imageAtlasTexture; + texSizeIcon = tile.imageAtlasTexture ? tile.imageAtlasTexture.size : [0, 0]; + atlasTextureIcon = tile.imageAtlasTexture ? tile.imageAtlasTexture : null; const zoomDependentSize = sizeData.kind === 'composite' || sizeData.kind === 'camera'; atlasInterpolationIcon = transformed || painter.options.rotating || painter.options.zooming || zoomDependentSize ? gl.LINEAR : gl.NEAREST; } } else { const iconScaled = layer.layout.get('icon-size').constantOr(0) !== 1 || bucket.iconsNeedLinear; - atlasTexture = tile.imageAtlasTexture; + atlasTexture = tile.imageAtlasTexture ? tile.imageAtlasTexture : null; atlasInterpolation = isSDF || painter.options.rotating || painter.options.zooming || iconScaled || transformed ? gl.LINEAR : gl.NEAREST; - texSize = tile.imageAtlasTexture.size; + texSize = tile.imageAtlasTexture ? tile.imageAtlasTexture.size : [0, 0]; } const bucketIsGlobeProjection = bucket.projection.name === 'globe'; @@ -463,7 +463,9 @@ function drawLayerSymbols(painter: Painter, sourceCache: SourceCache, layer: Sym painter.terrain.setupElevationDraw(state.tile, state.program, options); } context.activeTexture.set(gl.TEXTURE0); - state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); + if (state.atlasTexture) { + state.atlasTexture.bind(state.atlasInterpolation, gl.CLAMP_TO_EDGE); + } if (state.atlasTextureIcon) { context.activeTexture.set(gl.TEXTURE1); if (state.atlasTextureIcon) { diff --git a/src/render/painter.js b/src/render/painter.js index 54bd352f51b..9e0205e494e 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -1114,10 +1114,17 @@ class Painter { return translatedMatrix; } + /** + * Saves the tile texture for re-use when another tile is loaded. + * + * @returns true if the tile was cached, false if the tile was not cached and should be destroyed. + * @private + */ saveTileTexture(texture: Texture) { - const textures = this._tileTextures[texture.size[0]]; + const tileSize = texture.size[0]; + const textures = this._tileTextures[tileSize]; if (!textures) { - this._tileTextures[texture.size[0]] = [texture]; + this._tileTextures[tileSize] = [texture]; } else { textures.push(texture); } diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 46a0bf55ca8..e92aaf587d3 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -93,7 +93,7 @@ const lineUniformValues = ( 'u_dash_image': 0, 'u_gradient_image': 1, 'u_image_height': imageHeight, - 'u_texsize': hasDash(layer) ? tile.lineAtlasTexture.size : [0, 0], + 'u_texsize': hasDash(layer) && tile.lineAtlasTexture ? tile.lineAtlasTexture.size : [0, 0], 'u_tile_units_to_pixels': calculateTileRatio(tile, painter.transform), 'u_alpha_discard_threshold': 0.0, 'u_trim_offset': trimOffset, @@ -111,7 +111,7 @@ const linePatternUniformValues = ( const transform = painter.transform; return { 'u_matrix': calculateMatrix(painter, tile, layer, matrix), - 'u_texsize': tile.imageAtlasTexture.size, + 'u_texsize': tile.imageAtlasTexture ? tile.imageAtlasTexture.size : [0, 0], // camera zoom ratio 'u_pixels_to_tile_units': transform.calculatePixelsToTileUnitsMatrix(tile), 'u_device_pixel_ratio': pixelRatio, diff --git a/src/render/program/pattern.js b/src/render/program/pattern.js index fd29eb2605f..f366fe628c9 100644 --- a/src/render/program/pattern.js +++ b/src/render/program/pattern.js @@ -43,7 +43,7 @@ function patternUniformValues(painter: Painter, tile: Tile): UniformValues> 16, pixelY >> 16], diff --git a/src/source/custom_source.js b/src/source/custom_source.js index 289f1a2299e..ccd11459b51 100644 --- a/src/source/custom_source.js +++ b/src/source/custom_source.js @@ -327,6 +327,8 @@ class CustomSource extends Evented implements Source { this._implementation.unloadTile({x, y, z}); } + tile.destroy(); + callback(); } diff --git a/src/source/geojson_source.js b/src/source/geojson_source.js index a51c95e9b53..0c4ce939b9e 100644 --- a/src/source/geojson_source.js +++ b/src/source/geojson_source.js @@ -368,7 +368,7 @@ class GeoJSONSource extends Evented implements Source { tile.request = this.actor.send(message, params, (err, data) => { delete tile.request; - tile.unloadVectorData(); + tile.destroy(); if (tile.aborted) { return callback(null); @@ -395,8 +395,8 @@ class GeoJSONSource extends Evented implements Source { // $FlowFixMe[method-unbinding] unloadTile(tile: Tile) { - tile.unloadVectorData(); this.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id, scope: this.scope}); + tile.destroy(); } // $FlowFixMe[method-unbinding] diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 67e7515d294..6b8e10842bf 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -124,20 +124,6 @@ class RasterDEMTileSource extends RasterTileSource implements Source { return neighboringTiles; } - - // $FlowFixMe[method-unbinding] - unloadTile(tile: Tile) { - if (tile.demTexture) this.map.painter.saveTileTexture(tile.demTexture); - if (tile.hillshadeFBO) { - tile.hillshadeFBO.destroy(); - delete tile.hillshadeFBO; - } - if (tile.dem) delete tile.dem; - delete tile.neighboringTiles; - - tile.state = 'unloaded'; - } - } export default RasterDEMTileSource; diff --git a/src/source/raster_tile_source.js b/src/source/raster_tile_source.js index 5fa11c3f6d3..d3ff5412f8e 100644 --- a/src/source/raster_tile_source.js +++ b/src/source/raster_tile_source.js @@ -19,11 +19,12 @@ import type Dispatcher from '../util/dispatcher.js'; import type Tile from './tile.js'; import type {Callback} from '../types/callback.js'; import type {Cancelable} from '../types/cancelable.js'; -import type {TextureImage} from '../render/texture.js'; +import type {TextureImage} from "../render/texture.js"; import type { RasterSourceSpecification, RasterDEMSourceSpecification } from '../style-spec/types.js'; +import Texture from '../render/texture.js'; /** * A source containing raster tiles. @@ -221,10 +222,9 @@ class RasterTileSource extends Evented implements Source { tile.setTexture(data, painter); } + // eslint-disable-next-line no-unused-vars static unloadTileData(tile: Tile, painter: Painter) { - if (tile.texture) { - painter.saveTileTexture(tile.texture); - } + // Texture caching on unload occurs in unloadTile } // $FlowFixMe[method-unbinding] @@ -238,7 +238,20 @@ class RasterTileSource extends Evented implements Source { // $FlowFixMe[method-unbinding] unloadTile(tile: Tile, callback: Callback) { - if (tile.texture) this.map.painter.saveTileTexture(tile.texture); + // Cache the tile texture to avoid re-allocating Textures if they'll just be reloaded + if (tile.texture && tile.texture instanceof Texture) { + // Clean everything else up owned by the tile, but preserve the texture. + // Destroy first to prevent racing with the texture cache being popped. + tile.destroy(true); + + // Save the texture to the cache + if (tile.texture && tile.texture instanceof Texture) { + this.map.painter.saveTileTexture(tile.texture); + } + } else { + tile.destroy(); + } + callback(); } diff --git a/src/source/tile.js b/src/source/tile.js index 91a3c7984da..df1701fe950 100644 --- a/src/source/tile.js +++ b/src/source/tile.js @@ -56,7 +56,7 @@ import type {QueryResult} from '../data/feature_index.js'; import type Painter from '../render/painter.js'; import type {QueryFeature} from '../util/vectortile_to_geojson.js'; import type {Vec3} from 'gl-matrix'; -import type {TextureImage} from '../render/texture.js'; +import type {UserManagedTexture, TextureImage} from '../render/texture.js'; import type {VectorTileLayer} from '@mapbox/vector-tile'; const CLOCK_SKEW_RETRY_TIMEOUT = 30000; @@ -102,11 +102,11 @@ class Tile { latestFeatureIndex: ?FeatureIndex; latestRawTileData: ?ArrayBuffer; imageAtlas: ?ImageAtlas; - imageAtlasTexture: Texture; + imageAtlasTexture: ?Texture; lineAtlas: ?LineAtlas; - lineAtlasTexture: Texture; + lineAtlasTexture: ?Texture; glyphAtlasImage: ?AlphaImage; - glyphAtlasTexture: Texture; + glyphAtlasTexture: ?Texture; expirationTime: any; expiredRequestCount: number; state: TileState; @@ -129,7 +129,7 @@ class Tile { needsHillshadePrepare: ?boolean; needsDEMTextureUpload: ?boolean; request: ?Cancelable; - texture: any; + texture: ?Texture | ?UserManagedTexture; hillshadeFBO: ?Framebuffer; demTexture: ?Texture; refreshedUponExpiration: boolean; @@ -407,7 +407,7 @@ class Tile { } prepare(imageManager: ImageManager, painter: ?Painter, scope: string) { - if (this.imageAtlas) { + if (this.imageAtlas && this.imageAtlasTexture) { this.imageAtlas.patchUpdatedImages(imageManager, this.imageAtlasTexture, scope); } @@ -639,7 +639,7 @@ class Tile { const context = painter.context; const gl = context.gl; this.texture = this.texture || painter.getTileTexture(img.width); - if (this.texture) { + if (this.texture && this.texture instanceof Texture) { this.texture.update(img, {useMipmap: true}); } else { this.texture = new Texture(context, img, gl.RGBA, {useMipmap: true}); @@ -871,6 +871,113 @@ class Tile { this._globeTileDebugTextBuffer = context.createVertexBuffer(extraGlobe, posAttributesGlobeExt.members); this._tileDebugTextSegments = SegmentVector.simpleSegment(0, 0, totalVertices, totalTriangles); } + + /** + * Release data and WebGL resources referenced by this tile. + * @returns {undefined} + * @private + */ + destroy(preserveTexture: boolean = false) { + for (const id in this.buckets) { + this.buckets[id].destroy(); + } + + this.buckets = {}; + + if (this.imageAtlas) { + this.imageAtlas = null; + } + + if (this.lineAtlas) { + this.lineAtlas = null; + } + + if (this.imageAtlasTexture) { + this.imageAtlasTexture.destroy(); + delete this.imageAtlasTexture; + } + + if (this.glyphAtlasTexture) { + this.glyphAtlasTexture.destroy(); + delete this.glyphAtlasTexture; + } + + if (this.lineAtlasTexture) { + this.lineAtlasTexture.destroy(); + delete this.lineAtlasTexture; + } + + if (this._tileBoundsBuffer) { + this._tileBoundsBuffer.destroy(); + this._tileBoundsIndexBuffer.destroy(); + this._tileBoundsSegments.destroy(); + this._tileBoundsBuffer = null; + } + + if (this._tileDebugBuffer) { + this._tileDebugBuffer.destroy(); + this._tileDebugSegments.destroy(); + this._tileDebugBuffer = null; + } + + if (this._tileDebugIndexBuffer) { + this._tileDebugIndexBuffer.destroy(); + this._tileDebugIndexBuffer = null; + } + + if (this._globeTileDebugBorderBuffer) { + this._globeTileDebugBorderBuffer.destroy(); + this._globeTileDebugBorderBuffer = null; + } + + if (this._tileDebugTextBuffer) { + this._tileDebugTextBuffer.destroy(); + this._tileDebugTextSegments.destroy(); + this._tileDebugTextIndexBuffer.destroy(); + this._tileDebugTextBuffer = null; + } + + if (this._globeTileDebugTextBuffer) { + this._globeTileDebugTextBuffer.destroy(); + this._globeTileDebugTextBuffer = null; + } + + if (!preserveTexture && this.texture && this.texture instanceof Texture) { + this.texture.destroy(); + delete this.texture; + } + + if (this.hillshadeFBO) { + this.hillshadeFBO.destroy(); + delete this.hillshadeFBO; + } + + if (this.dem) { + delete this.dem; + } + + if (this.neighboringTiles) { + delete this.neighboringTiles; + } + + if (this.demTexture) { + this.demTexture.destroy(); + delete this.demTexture; + } + + Debug.run(() => { + if (this.queryGeometryDebugViz) { + this.queryGeometryDebugViz.unload(); + delete this.queryGeometryDebugViz; + } + if (this.queryBoundsDebugViz) { + this.queryBoundsDebugViz.unload(); + delete this.queryBoundsDebugViz; + } + }); + this.latestFeatureIndex = null; + this.state = 'unloaded'; + } } export default Tile; diff --git a/src/source/vector_tile_source.js b/src/source/vector_tile_source.js index 7a42b534e42..73265fb1a6b 100644 --- a/src/source/vector_tile_source.js +++ b/src/source/vector_tile_source.js @@ -312,10 +312,10 @@ class VectorTileSource extends Evented implements Source { // $FlowFixMe[method-unbinding] unloadTile(tile: Tile) { - tile.unloadVectorData(); if (tile.actor) { tile.actor.send('removeTile', {uid: tile.uid, type: this.type, source: this.id, scope: this.scope}); } + tile.destroy(); } hasTransition(): boolean { diff --git a/src/terrain/draw_terrain_raster.js b/src/terrain/draw_terrain_raster.js index a6c7eea4fe2..6fcef850582 100644 --- a/src/terrain/draw_terrain_raster.js +++ b/src/terrain/draw_terrain_raster.js @@ -191,7 +191,9 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So // Bind the main draped texture context.activeTexture.set(gl.TEXTURE0); - tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + if (tile.texture) { + tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } const morph = vertexMorphing.getMorphValuesForProxy(coord.key); const shaderMode = morph ? SHADER_MORPHING : SHADER_DEFAULT; @@ -246,7 +248,9 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So // Bind the main draped texture context.activeTexture.set(gl.TEXTURE0); - tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + if (tile.texture) { + tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } let poleMatrix = globePoleMatrixForTile(z, x, tr); const normalizeMatrix = globeNormalizeECEF(globeTileBounds(coord.canonical)); @@ -331,7 +335,9 @@ function drawTerrainRaster(painter: Painter, terrain: Terrain, sourceCache: Sour // Bind the main draped texture context.activeTexture.set(gl.TEXTURE0); - tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); + if (tile.texture) { + tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE); + } const morph = vertexMorphing.getMorphValuesForProxy(coord.key); const shaderMode = morph ? SHADER_MORPHING : SHADER_DEFAULT;