Skip to content

Commit

Permalink
Improve texture memory usage, by unloading tiles, partly resolving #1…
Browse files Browse the repository at this point in the history
…2906 (#12924)

* Consistent Tile.destroy() with texture caching

* Fix up potential race

Co-authored-by: Volodymyr Agafonkin <agafonkin@gmail.com>
  • Loading branch information
tristan-morris and mourner committed Dec 4, 2023
1 parent dd10457 commit 94b9007
Show file tree
Hide file tree
Showing 16 changed files with 392 additions and 56 deletions.
201 changes: 201 additions & 0 deletions debug/pathological-flyto.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<!--
This is useful for debugging memory usage.
-->
<!DOCTYPE html>
<script
src="https://greggman.github.io/webgl-memory/webgl-memory.js"
crossorigin
></script>
<html>

<head>
<title>Mapbox GL JS Pathological FlyTo</title>
<meta charset='utf-8'>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
>
<link
rel='stylesheet'
href='../dist/mapbox-gl.css'
/>
<style>
body {
margin: 0;
padding: 0;
}

#overlay {
position: absolute;
color: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
text-shadow: 0px 2px 2px black;
z-index: 100;
font-size: 150%;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
}

html,
body,
#map {
height: 100%;
}
</style>
</head>

<body>
<div style="padding: 5px;">
<button id="start">Start Flying Around</button>
<button id="cancel">Stop Flying Around</button>
<button id="loadSatellite">Load Satellite Streets</button>
<button id="loadTerrain">Load Terrain</button>
<button id="loadGeo">Load Geojson</button>
</div>
<div id='map'>
<div id="overlay"></div>
</div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>
<script>
let goat = 0;
let pleaseStop = null;
let totals = [];

document.getElementById("start").addEventListener("click", () => {
totals = [];
pleaseStop = setInterval(() => {
if ((goat % 2) === 0) {
map.flyTo({
center: [-122.41381885225483 + Math.random() * 20, 37.73787623085941],
zoom: 16,
duration: 7000,
preloadOnly: false
});
} else {
map.flyTo({
center: [148.44217489435937 - Math.random() * 10, -35.721640804238895],
zoom: 16,
duration: 7000,
preloadOnly: false
});
}

goat++;

}, 5000);
});

document.getElementById("cancel").addEventListener("click", () => {
window.pleaseStop = clearInterval(pleaseStop);
});

document.getElementById("loadGeo").addEventListener("click", () => {
totals = [];
map.addSource('earthquakes', {
type: 'geojson',
// Use a URL for the value for the `data` property.
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson'
});

map.addLayer({
'id': 'earthquakes-layer',
'type': 'circle',
'source': 'earthquakes',
'paint': {
'circle-radius': 4,
'circle-stroke-width': 2,
'circle-color': 'red',
'circle-stroke-color': 'white'
}
});

});

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 12.5,
center: [-122.4194, 37.7749],
hash: false,
style: "mapbox://styles/mapbox/streets-v11?optimize=true"

});

document.getElementById("loadTerrain").addEventListener("click", () => {
totals = [];
map.addSource('mapbox-dem', {
'type': 'raster-dem',
'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
'tileSize': 512,
'maxzoom': 14
});

map.setTerrain({'source': 'mapbox-dem', 'exaggeration': 1.5});
});

document.getElementById("loadSatellite").addEventListener("click", () => {
totals = [];
map.setStyle("mapbox://styles/mapbox/satellite-streets-v11?optimize=true");
});

const gl = map.getCanvas().getContext("webgl2");
const ext = gl.getExtension('GMAN_webgl_memory');
const gpu = ext.getMemoryInfo();
let prev = gpu;
let prevMax = 0;
let prevAvg = 0;
console.log("memory info", gpu);

// Limit moving average to 1000 items
const limit = (arr) => {
if (arr.length > 1000) {
arr.shift();
}
};

// Calculate a moving average for a number
const movingAverage = (arr) => {
const count = arr.length;
const sum = arr.reduce((a, b) => a + b, 0);
return Math.round(sum / count);
};

// Calculate the maximum value in an array
const max = (arr) => {
return arr.reduce((a, b) => Math.max(a, b));
};

const highlightDiff = (current, previous, count = 0) => {
if (current > previous) {
return `<span style="color: red">${count === 0 ? "" : count + " / "}${Math.round(current / 1024 / 1024)}<small>MB</small></span>`;
} else {
return `<span style="color: green">${count === 0 ? "" : count + " / "}${Math.round(current / 1024 / 1024)}<small>MB</small></span>`;
}
};

setInterval(() => {
const gpu = ext.getMemoryInfo();
const overlay = document.getElementById("overlay");

totals.push(gpu.memory.total);
const currAvg = movingAverage(totals);
const currMax = max(totals);
limit(totals);

let html = `GPU Memory <br />`;
html += `total: ${highlightDiff(gpu.memory.total, prev.memory.total)} (avg: ${highlightDiff(currAvg, prevAvg)}; max: ${highlightDiff(currMax, prevMax)}) <br />`;
html += `drawingbuffer: ${highlightDiff(gpu.memory.drawingbuffer, prev.memory.drawingbuffer)} <br />`;
html += `buffer: ${highlightDiff(gpu.memory.buffer, prev.memory.buffer, gpu.resources.buffer)} <br />`;
html += `texture: ${highlightDiff(gpu.memory.texture, prev.memory.texture, gpu.resources.texture)} <br />`;
overlay.innerHTML = html;
totals.push(gpu.memory.total);

prev = gpu;
prevMax = currMax;
prevAvg = currAvg;
}, 50);
</script>
</body>

</html>
4 changes: 3 additions & 1 deletion src/render/draw_fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
4 changes: 3 additions & 1 deletion src/render/draw_fill_extrusion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions src/render/draw_line.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,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();
}

Expand Down
12 changes: 8 additions & 4 deletions src/render/draw_raster.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
24 changes: 13 additions & 11 deletions src/render/draw_symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -49,7 +49,7 @@ type SymbolTileRenderState = {
program: any,
buffers: SymbolBuffers,
uniformValues: any,
atlasTexture: Texture,
atlasTexture: Texture | null,
atlasTextureIcon: Texture | null,
atlasInterpolation: any,
atlasInterpolationIcon: any,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 9 additions & 2 deletions src/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1120,10 +1120,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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/render/program/line_program.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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,
Expand All @@ -110,7 +110,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,
Expand Down
2 changes: 1 addition & 1 deletion src/render/program/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function patternUniformValues(painter: Painter, tile: Tile): UniformValues<Patte

return {
'u_image': 0,
'u_texsize': tile.imageAtlasTexture.size,
'u_texsize': tile.imageAtlasTexture ? tile.imageAtlasTexture.size : [0, 0],
'u_tile_units_to_pixels': 1 / pixelsToTileUnits(tile, 1, painter.transform.tileZoom),
// split the pixel coord into two pairs of 16 bit numbers. The glsl spec only guarantees 16 bits of precision.
'u_pixel_coord_upper': [pixelX >> 16, pixelY >> 16],
Expand Down
2 changes: 2 additions & 0 deletions src/source/custom_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ class CustomSource<T> extends Evented implements Source {
this._implementation.unloadTile({x, y, z});
}

tile.destroy();

callback();
}

Expand Down

0 comments on commit 94b9007

Please sign in to comment.