Skip to content

Commit

Permalink
WebGL matrix performance fix (#5072)
Browse files Browse the repository at this point in the history
* Use float32 for WebGL matrices

* Fix tests

* Fix render tests

* Use Map instead of js object, use f64 matrices for non-webgl

* Fix matrix cache access

* Revert mercator transform tests to expect high-precision numbers

* Update render tests

* Remove unnecessary matrix creation

* Make sure RTT matrices are also float32

* Fix failing unit test

* Use an object for both f32 and f64 matrix in mercator transform

* Update changelog & build size
  • Loading branch information
kubapelc authored Nov 25, 2024
1 parent 9b27fa0 commit 30ab703
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 54 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### 🐞 Bug fixes
- Fixes line flickering problem ([#5094](https://github.com/maplibre/maplibre-gl-js/pull/5094))
- Fix poor performance in Chrome related to passing matrices to WebGL ([#5072](https://github.com/maplibre/maplibre-gl-js/pull/5072))
- _...Add new stuff here..._

## 5.0.0-pre.7
Expand Down
10 changes: 5 additions & 5 deletions src/geo/projection/globe_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {type mat2, mat4, vec3, vec4} from 'gl-matrix';
import {MAX_VALID_LATITUDE, TransformHelper} from '../transform_helper';
import {MercatorTransform} from './mercator_transform';
import {LngLat, type LngLatLike, earthRadius} from '../lng_lat';
import {angleToRotateBetweenVectors2D, clamp, createIdentityMat4f64, createMat4f64, createVec3f64, createVec4f64, differenceOfAnglesDegrees, distanceOfAnglesRadians, easeCubicInOut, lerp, pointPlaneSignedDistance, warnOnce} from '../../util/util';
import {angleToRotateBetweenVectors2D, clamp, createIdentityMat4f32, createIdentityMat4f64, createMat4f32, createMat4f64, createVec3f64, createVec4f64, differenceOfAnglesDegrees, distanceOfAnglesRadians, easeCubicInOut, lerp, pointPlaneSignedDistance, warnOnce} from '../../util/util';
import {UnwrappedTileID, OverscaledTileID, type CanonicalTileID} from '../../source/tile_id';
import Point from '@mapbox/point-geometry';
import {browser} from '../../util/browser';
Expand Down Expand Up @@ -240,7 +240,7 @@ export class GlobeTransform implements ITransform {
private _skipNextAnimation: boolean = true;

private _projectionMatrix: mat4 = createIdentityMat4f64();
private _globeViewProjMatrix: mat4 = createIdentityMat4f64();
private _globeViewProjMatrix32f: mat4 = createIdentityMat4f32(); // Must be 32 bit floats, otherwise WebGL calls in Chrome get very slow.
private _globeViewProjMatrixNoCorrection: mat4 = createIdentityMat4f64();
private _globeViewProjMatrixNoCorrectionInverted: mat4 = createIdentityMat4f64();
private _globeProjMatrixInverted: mat4 = createIdentityMat4f64();
Expand Down Expand Up @@ -459,7 +459,7 @@ export class GlobeTransform implements ITransform {

// Set 'projectionMatrix' to actual globe transform
if (this.isGlobeRendering) {
data.mainMatrix = this._globeViewProjMatrix;
data.mainMatrix = this._globeViewProjMatrix32f;
}

data.clippingPlane = this._cachedClippingPlane as [number, number, number, number];
Expand Down Expand Up @@ -661,7 +661,7 @@ export class GlobeTransform implements ITransform {
mat4.rotateX(globeMatrix, globeMatrix, this.center.lat * Math.PI / 180.0 - this._globeLatitudeErrorCorrectionRadians);
mat4.rotateY(globeMatrix, globeMatrix, -this.center.lng * Math.PI / 180.0);
mat4.scale(globeMatrix, globeMatrix, scaleVec); // Scale the unit sphere to a sphere with diameter of 1
this._globeViewProjMatrix = globeMatrix;
this._globeViewProjMatrix32f = new Float32Array(globeMatrix);

this._globeViewProjMatrixNoCorrectionInverted = createMat4f64();
mat4.invert(this._globeViewProjMatrixNoCorrectionInverted, globeMatrixUncorrected);
Expand Down Expand Up @@ -1194,7 +1194,7 @@ export class GlobeTransform implements ITransform {
// the fallback projection matrix by EXTENT.
// Note that the regular projection matrices do not need to be modified, since the rescaling happens by setting
// the `u_projection_tile_mercator_coords` uniform correctly.
const fallbackMatrixScaled = createMat4f64();
const fallbackMatrixScaled = createMat4f32();
mat4.scale(fallbackMatrixScaled, projectionData.fallbackMatrix, [EXTENT, EXTENT, 1]);

projectionData.fallbackMatrix = fallbackMatrixScaled;
Expand Down
44 changes: 25 additions & 19 deletions src/geo/projection/mercator_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ export class MercatorTransform implements ITransform {
private _pixelMatrixInverse: mat4;
private _fogMatrix: mat4;

private _posMatrixCache: {[_: string]: mat4};
private _fogMatrixCache: {[_: string]: mat4};
private _alignedPosMatrixCache: {[_: string]: mat4};
private _posMatrixCache: Map<string, {f64: mat4; f32: mat4}> = new Map();
private _alignedPosMatrixCache: Map<string, {f64: mat4; f32: mat4}> = new Map();
private _fogMatrixCacheF32: Map<string, mat4> = new Map();

private _nearZ;
private _farZ;
Expand All @@ -224,7 +224,6 @@ export class MercatorTransform implements ITransform {
calcMatrices: () => { this._calcMatrices(); },
getConstrained: (center, zoom) => { return this.getConstrained(center, zoom); }
}, minZoom, maxZoom, minPitch, maxPitch, renderWorldCopies);
this._clearMatrixCaches();
this._coveringTilesDetailsProvider = new MercatorCoveringTilesDetailsProvider();
}

Expand Down Expand Up @@ -405,33 +404,39 @@ export class MercatorTransform implements ITransform {
* This function is specific to the mercator projection.
* @param tileID - the tile ID
* @param aligned - whether to use a pixel-aligned matrix variant, intended for rendering raster tiles
* @param useFloat32 - when true, returns a float32 matrix instead of float64. Use float32 for matrices that are passed to shaders, use float64 for everything else.
*/
calculatePosMatrix(tileID: UnwrappedTileID | OverscaledTileID, aligned: boolean = false): mat4 {
calculatePosMatrix(tileID: UnwrappedTileID | OverscaledTileID, aligned: boolean = false, useFloat32?: boolean): mat4 {
const posMatrixKey = tileID.key ?? calculateTileKey(tileID.wrap, tileID.canonical.z, tileID.canonical.z, tileID.canonical.x, tileID.canonical.y);
const cache = aligned ? this._alignedPosMatrixCache : this._posMatrixCache;
if (cache[posMatrixKey]) {
return cache[posMatrixKey];
if (cache.has(posMatrixKey)) {
const matrices = cache.get(posMatrixKey);
return useFloat32 ? matrices.f32 : matrices.f64;
}

const tileMatrix = calculateTileMatrix(tileID, this.worldSize);
mat4.multiply(tileMatrix, aligned ? this._alignedProjMatrix : this._viewProjMatrix, tileMatrix);

cache[posMatrixKey] = tileMatrix;
return cache[posMatrixKey];
const matrices = {
f64: tileMatrix,
f32: new Float32Array(tileMatrix), // Must have a 32 bit float version for WebGL, otherwise WebGL calls in Chrome get very slow.
}
cache.set(posMatrixKey, matrices);
// Make sure to return the correct precision
return useFloat32 ? matrices.f32 : matrices.f64;
}

calculateFogMatrix(unwrappedTileID: UnwrappedTileID): mat4 {
const posMatrixKey = unwrappedTileID.key;
const cache = this._fogMatrixCache;
if (cache[posMatrixKey]) {
return cache[posMatrixKey];
const cache = this._fogMatrixCacheF32;
if (cache.has(posMatrixKey)) {
return cache.get(posMatrixKey);
}

const fogMatrix = calculateTileMatrix(unwrappedTileID, this.worldSize);
mat4.multiply(fogMatrix, this._fogMatrix, fogMatrix);

cache[posMatrixKey] = fogMatrix;
return cache[posMatrixKey];
cache.set(posMatrixKey, new Float32Array(fogMatrix)); // Must be 32 bit floats, otherwise WebGL calls in Chrome get very slow.
return cache.get(posMatrixKey);
}

/**
Expand Down Expand Up @@ -713,9 +718,9 @@ export class MercatorTransform implements ITransform {
}

private _clearMatrixCaches(): void {
this._posMatrixCache = {};
this._alignedPosMatrixCache = {};
this._fogMatrixCache = {};
this._posMatrixCache.clear();
this._alignedPosMatrixCache.clear();
this._fogMatrixCacheF32.clear();
}

maxPitchScaleFactor(): number {
Expand Down Expand Up @@ -760,7 +765,7 @@ export class MercatorTransform implements ITransform {

getProjectionData(params: ProjectionDataParams): ProjectionData {
const {overscaledTileID, aligned, applyTerrainMatrix} = params;
const matrix = overscaledTileID ? this.calculatePosMatrix(overscaledTileID, aligned) : null;
const matrix = overscaledTileID ? this.calculatePosMatrix(overscaledTileID, aligned, true) : null;
return getBasicProjectionData(overscaledTileID, matrix, applyTerrainMatrix);
}

Expand Down Expand Up @@ -849,6 +854,7 @@ export class MercatorTransform implements ITransform {

const scale: vec3 = [EXTENT, EXTENT, this.worldSize / this._helper.pixelsPerMeter];

// We pass full-precision 64bit float matrices to custom layers to prevent precision loss in case the user wants to do further transformations.
const fallbackMatrixScaled = createMat4f64();
mat4.scale(fallbackMatrixScaled, tileMatrix, scale);

Expand Down
10 changes: 5 additions & 5 deletions src/geo/projection/mercator_utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {mat4} from 'gl-matrix';
import {EXTENT} from '../../data/extent';
import {type OverscaledTileID} from '../../source/tile_id';
import {clamp, degreesToRadians} from '../../util/util';
import {clamp, createIdentityMat4f32, degreesToRadians} from '../../util/util';
import {MAX_VALID_LATITUDE, type UnwrappedTileIDType, zoomScale} from '../transform_helper';
import {type LngLat} from '../lng_lat';
import {MercatorCoordinate, mercatorXfromLng, mercatorYfromLat, mercatorZfromAltitude} from '../mercator_coordinate';
Expand Down Expand Up @@ -110,12 +110,12 @@ export function getBasicProjectionData(overscaledTileID: OverscaledTileID, tileP
}

let mainMatrix: mat4;
if (overscaledTileID && overscaledTileID.terrainRttPosMatrix && applyTerrainMatrix) {
mainMatrix = overscaledTileID.terrainRttPosMatrix;
if (overscaledTileID && overscaledTileID.terrainRttPosMatrix32f && applyTerrainMatrix) {
mainMatrix = overscaledTileID.terrainRttPosMatrix32f;
} else if (tilePosMatrix) {
mainMatrix = tilePosMatrix;
mainMatrix = tilePosMatrix; // This matrix should be float32
} else {
mainMatrix = mat4.create();
mainMatrix = createIdentityMat4f32();
}

const data: ProjectionData = {
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_fill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ describe('drawFill', () => {

function constructMockTile(layer: FillStyleLayer): Tile {
const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.terrainRttPosMatrix = mat4.create();
tileId.terrainRttPosMatrix32f = mat4.create();

const tile = new Tile(tileId, 256);
tile.tileID = tileId;
Expand Down
6 changes: 3 additions & 3 deletions src/render/draw_symbol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('drawSymbol', () => {
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);

const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.terrainRttPosMatrix = mat4.create();
tileId.terrainRttPosMatrix32f = mat4.create();
const programMock = new Program(null, null, null, null, null, null, null, null);
(painterMock.useProgram as Mock).mockReturnValue(programMock);
const bucketMock = new SymbolBucket(null);
Expand Down Expand Up @@ -153,7 +153,7 @@ describe('drawSymbol', () => {
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);

const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.terrainRttPosMatrix = mat4.create();
tileId.terrainRttPosMatrix32f = mat4.create();
const programMock = new Program(null, null, null, null, null, null, null, null);
(painterMock.useProgram as Mock).mockReturnValue(programMock);
const bucketMock = new SymbolBucket(null);
Expand Down Expand Up @@ -220,7 +220,7 @@ describe('drawSymbol', () => {
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters, []);

const tileId = new OverscaledTileID(1, 0, 1, 0, 0);
tileId.terrainRttPosMatrix = mat4.create();
tileId.terrainRttPosMatrix32f = mat4.create();
const programMock = new Program(null, null, null, null, null, null, null, null);
(painterMock.useProgram as Mock).mockReturnValue(programMock);
const bucketMock = new SymbolBucket(null);
Expand Down
2 changes: 1 addition & 1 deletion src/render/render_to_texture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('render to texture', () => {
'key': '923',
'overscaledZ': 3,
'wrap': 0,
'terrainRttPosMatrix': null,
'terrainRttPosMatrix32f': null,
}
]
}
Expand Down
32 changes: 15 additions & 17 deletions src/source/terrain_source_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {SourceCache} from '../source/source_cache';
import {type Terrain} from '../render/terrain';
import {browser} from '../util/browser';
import {coveringTiles} from '../geo/projection/covering_tiles';
import {createMat4f64} from '../util/util';

/**
* @internal
Expand Down Expand Up @@ -98,8 +99,8 @@ export class TerrainSourceCache extends Evented {
keys[tileID.key] = true;
this._renderableTilesKeys.push(tileID.key);
if (!this._tiles[tileID.key]) {
tileID.terrainRttPosMatrix = new Float64Array(16) as any;
mat4.ortho(tileID.terrainRttPosMatrix, 0, EXTENT, EXTENT, 0, 0, 1);
tileID.terrainRttPosMatrix32f = new Float64Array(16) as any;
mat4.ortho(tileID.terrainRttPosMatrix32f, 0, EXTENT, EXTENT, 0, 0, 1);
this._tiles[tileID.key] = new Tile(tileID, this.tileSize);
this._lastTilesetChange = browser.now();
}
Expand Down Expand Up @@ -148,33 +149,30 @@ export class TerrainSourceCache extends Evented {
const coords = {};
for (const key of this._renderableTilesKeys) {
const _tileID = this._tiles[key].tileID;
const coord = tileID.clone();
const mat = createMat4f64();
if (_tileID.canonical.equals(tileID.canonical)) {
const coord = tileID.clone();
coord.terrainRttPosMatrix = new Float64Array(16) as any;
mat4.ortho(coord.terrainRttPosMatrix, 0, EXTENT, EXTENT, 0, 0, 1);
coords[key] = coord;
mat4.ortho(mat, 0, EXTENT, EXTENT, 0, 0, 1);
} else if (_tileID.canonical.isChildOf(tileID.canonical)) {
const coord = tileID.clone();
coord.terrainRttPosMatrix = new Float64Array(16) as any;
const dz = _tileID.canonical.z - tileID.canonical.z;
const dx = _tileID.canonical.x - (_tileID.canonical.x >> dz << dz);
const dy = _tileID.canonical.y - (_tileID.canonical.y >> dz << dz);
const size = EXTENT >> dz;
mat4.ortho(coord.terrainRttPosMatrix, 0, size, size, 0, 0, 1); // Note: we are using `size` instead of `EXTENT` here
mat4.translate(coord.terrainRttPosMatrix, coord.terrainRttPosMatrix, [-dx * size, -dy * size, 0]);
coords[key] = coord;
mat4.ortho(mat, 0, size, size, 0, 0, 1); // Note: we are using `size` instead of `EXTENT` here
mat4.translate(mat, mat, [-dx * size, -dy * size, 0]);
} else if (tileID.canonical.isChildOf(_tileID.canonical)) {
const coord = tileID.clone();
coord.terrainRttPosMatrix = new Float64Array(16) as any;
const dz = tileID.canonical.z - _tileID.canonical.z;
const dx = tileID.canonical.x - (tileID.canonical.x >> dz << dz);
const dy = tileID.canonical.y - (tileID.canonical.y >> dz << dz);
const size = EXTENT >> dz;
mat4.ortho(coord.terrainRttPosMatrix, 0, EXTENT, EXTENT, 0, 0, 1);
mat4.translate(coord.terrainRttPosMatrix, coord.terrainRttPosMatrix, [dx * size, dy * size, 0]);
mat4.scale(coord.terrainRttPosMatrix, coord.terrainRttPosMatrix, [1 / (2 ** dz), 1 / (2 ** dz), 0]);
coords[key] = coord;
mat4.ortho(mat, 0, EXTENT, EXTENT, 0, 0, 1);
mat4.translate(mat, mat, [dx * size, dy * size, 0]);
mat4.scale(mat, mat, [1 / (2 ** dz), 1 / (2 ** dz), 0]);
} else {
continue;
}
coord.terrainRttPosMatrix32f = new Float32Array(mat);
coords[key] = coord;
}
return coords;
}
Expand Down
5 changes: 3 additions & 2 deletions src/source/tile_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ export class OverscaledTileID {
* This matrix is used during terrain's render-to-texture stage only.
* If the render-to-texture stage is active, this matrix will be present
* and should be used, otherwise this matrix will be null.
* The matrix should be float32 in order to avoid slow WebGL calls in Chrome.
*/
terrainRttPosMatrix: mat4 | null = null;
terrainRttPosMatrix32f: mat4 | null = null;

constructor(overscaledZ: number, wrap: number, z: number, x: number, y: number) {
if (overscaledZ < z) throw new Error(`overscaledZ should be >= z; overscaledZ = ${overscaledZ}; z = ${z}`);
Expand Down Expand Up @@ -223,4 +224,4 @@ function getQuadkey(z, x, y) {
}

register('CanonicalTileID', CanonicalTileID);
register('OverscaledTileID', OverscaledTileID, {omit: ['terrainRttPosMatrix']});
register('OverscaledTileID', OverscaledTileID, {omit: ['terrainRttPosMatrix32f']});
12 changes: 12 additions & 0 deletions src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export function createVec3f64(): vec3 { return new Float64Array(3) as any; }
* Returns a new 64 bit float mat4 of zeroes.
*/
export function createMat4f64(): mat4 { return new Float64Array(16) as any; }
/**
* Returns a new 32 bit float mat4 of zeroes.
*/
export function createMat4f32(): mat4 { return new Float32Array(16) as any; }
/**
* Returns a new 64 bit float mat4 set to identity.
*/
Expand All @@ -27,6 +31,14 @@ export function createIdentityMat4f64(): mat4 {
mat4.identity(m);
return m;
}
/**
* Returns a new 32 bit float mat4 set to identity.
*/
export function createIdentityMat4f32(): mat4 {
const m = new Float32Array(16) as any;
mat4.identity(m);
return m;
}

/**
* Returns a translation in tile units that correctly incorporates the view angle and the *-translate and *-translate-anchor properties.
Expand Down
2 changes: 1 addition & 1 deletion test/build/min.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('test min build', () => {
const decreaseQuota = 4096;

// feel free to update this value after you've checked that it has changed on purpose :-)
const expectedBytes = 898319;
const expectedBytes = 898930;

expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota);
expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 30ab703

Please sign in to comment.