Skip to content

Commit 7499aa3

Browse files
xavierjsHarelM
andauthored
Optimize matrix inversions and reduce GPU stalls (#7367)
* [QUAL] Optimize matrix computation, avoid some matrix allocations, small optimizations * [MINOR] * [DOC] Update changelog * [QUAL] Make unit test pass - if WebGL2RenderingContext is not defined * [QUAL] Add unit tests for matrix inversion [FIX] Fix a skew inversion matrix bug thanx to the unit test * [QUAL] Add more tests for fast_maths * [QUAL] Implement feedback * [MINOR] * [MINOR] * [QUAL] Resolve #7367 (comment) * [TEST] Update min bundle size test result * [MINOR] --------- Co-authored-by: Harel M <harel.mazor@gmail.com>
1 parent f1d9a91 commit 7499aa3

10 files changed

Lines changed: 452 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## main
22
### ✨ Features and improvements
33
- _...Add new stuff here..._
4+
- Optimize matrix inversions and reduce GPU stalls ([#7367](https://github.com/maplibre/maplibre-gl-js/pull/7367)) (by [@xavierjs](https://github.com/xavierjs))
5+
- Add example showing how to measure map performance using built-in events (`load`, `idle`, `render`) ([#7077](https://github.com/maplibre/maplibre-gl-js/pull/7077)) (by [@CommanderStorm](https://github.com/CommanderStorm))
6+
- Add `touchZoomRotate.setZoomRate()` and `touchZoomRotate.setZoomThreshold()` to customize touch zoom speed and pinch sensitivity ([#7271](https://github.com/maplibre/maplibre-gl-js/issues/7271))
47

58
### 🐞 Bug fixes
69
- Fix `Popup` not updating its position when switching between terrain/globe projections ([#7468](https://github.com/maplibre/maplibre-gl-js/pull/7468)) (by [@CommanderStorm](https://github.com/CommanderStorm))

src/geo/projection/mercator_transform.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {EXTENT} from '../../data/extent';
1212
import {TransformHelper} from '../transform_helper';
1313
import {MercatorCoveringTilesDetailsProvider} from './mercator_covering_tiles_details_provider';
1414
import {Frustum} from '../../util/primitives/frustum';
15+
import {fastInvertProjMat4} from '../../util/fast_maths';
1516

1617
import type {Terrain} from '../../render/terrain';
1718
import type {IReadonlyTransform, ITransform, TransformConstrainFunction} from '../transform_interface';
@@ -611,7 +612,7 @@ export class MercatorTransform implements ITransform {
611612
m = new Float64Array(16) as any;
612613
mat4.perspective(m, this.fovInRadians, this._helper._width / this._helper._height, this._helper._nearZ, this._helper._farZ);
613614
this._invProjMatrix = new Float64Array(16) as any as mat4;
614-
mat4.invert(this._invProjMatrix, m);
615+
fastInvertProjMat4(this._invProjMatrix, m);
615616

616617
// Apply center of perspective offset
617618
m[8] = -offset.x * 2 / this._helper._width;

src/symbol/projection.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import {WritingMode} from '../symbol/shaping';
1717
import {findLineIntersection} from '../util/util';
1818
import {type UnwrappedTileID} from '../tile/tile_id';
1919
import {type StructArray} from '../util/struct_array';
20+
import {fastInvertSkewMat4} from '../util/fast_maths';
21+
22+
/**
23+
* Pre-allocate objects to avoid online allocation
24+
*/
25+
const tmpMat4 = mat4.create();
2026

2127
/**
2228
* The result of projecting a point to the screen, with some additional information about the projection.
@@ -931,8 +937,8 @@ export function xyTransformMat4(out: vec4, a: vec4, m: mat4) {
931937
* Does not modify the input array.
932938
*/
933939
export function projectPathSpecialProjection(projectedPath: Point[], projectionContext: SymbolProjectionContext): PointProjection[] {
934-
const inverseLabelPlaneMatrix = mat4.create();
935-
mat4.invert(inverseLabelPlaneMatrix, projectionContext.pitchedLabelPlaneMatrix);
940+
const inverseLabelPlaneMatrix = tmpMat4;
941+
fastInvertSkewMat4(inverseLabelPlaneMatrix, projectionContext.pitchedLabelPlaneMatrix);
936942
return projectedPath.map(p => {
937943
const backProjected = projectWithMatrix(p.x, p.y, inverseLabelPlaneMatrix, projectionContext.getElevation);
938944
const projected = projectionContext.transform.projectTileCoordinates(

src/util/fast_maths.test.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import {describe, test, expect} from 'vitest';
2+
import {mat4, vec3, quat} from 'gl-matrix';
3+
import {fastInvertTransformMat4, fastInvertProjMat4, fastInvertSkewMat4} from './fast_maths';
4+
5+
function compare_matrix(mat: mat4, matRef: mat4){
6+
for (let i=0; i<16; ++i){
7+
expect(mat[i]).toBeCloseTo(matRef[i], 6);
8+
}
9+
}
10+
11+
describe('fast_maths', () => {
12+
test('invert a transform matrix', () => {
13+
const testParams = [{
14+
translate: [1, 2, -3],
15+
scaleXY: 10,
16+
rotAxis: [5, 6, 7],
17+
rotAngle: Math.PI / 4
18+
},{
19+
translate: [0, 0, 0],
20+
scaleXY: 1,
21+
rotAxis: [0, 0, 1],
22+
rotAngle: 0
23+
},{
24+
translate: [-50, 100, 0.5],
25+
scaleXY: 0.01,
26+
rotAxis: [1, 0, 0],
27+
rotAngle: Math.PI
28+
},{
29+
translate: [1000, -2000, 500],
30+
scaleXY: 200,
31+
rotAxis: [0, 1, 0],
32+
rotAngle: -Math.PI / 6
33+
},{
34+
translate: [-0.1, 0.2, -0.3],
35+
scaleXY: 0.5,
36+
rotAxis: [1, 1, 0],
37+
rotAngle: Math.PI / 2
38+
},{
39+
translate: [0, 0, -9999],
40+
scaleXY: 50,
41+
rotAxis: [0.3, 0.4, 0.5],
42+
rotAngle: -0.01
43+
},{
44+
translate: [42, -7, 13],
45+
scaleXY: 4,
46+
rotAxis: [1, -2, 3],
47+
rotAngle: 2.1
48+
}];
49+
const m = mat4.create();
50+
const mInv = mat4.create();
51+
const mInvRef = mat4.create();
52+
for (const {translate, scaleXY, rotAxis, rotAngle} of testParams) {
53+
// translation vector:
54+
const v = vec3.create();
55+
vec3.copy(v, translate);
56+
57+
// scale vector. sx shoule be = sy:
58+
const s = vec3.create();
59+
s[0] = scaleXY;
60+
s[1] = scaleXY;
61+
s[2] = 1;
62+
63+
// quaternion:
64+
const q = quat.create();
65+
const axis = vec3.create();
66+
vec3.copy(axis, rotAxis);
67+
vec3.normalize(axis, axis);
68+
quat.setAxisAngle(q, axis, rotAngle);
69+
mat4.fromRotationTranslationScale(m, q, v, s);
70+
71+
// compute inv:
72+
fastInvertTransformMat4(mInv, m);
73+
74+
// compute ref:
75+
mat4.invert(mInvRef, m);
76+
77+
compare_matrix(mInv, mInvRef);
78+
};
79+
});
80+
81+
test('invert a projection matrix', () => {
82+
const testParams = [
83+
{
84+
fov: Math.PI/4,
85+
aspect: 16/9,
86+
zNear: 1,
87+
zFar: 1000
88+
},
89+
{
90+
fov: Math.PI/3,
91+
aspect: 4/3,
92+
zNear: 10,
93+
zFar: 10000
94+
},
95+
{
96+
fov: Math.PI/8,
97+
aspect: 1,
98+
zNear: 1,
99+
zFar: 10
100+
},
101+
{
102+
fov: Math.PI/4,
103+
aspect: 2,
104+
zNear: 1,
105+
zFar: 1000
106+
}
107+
];
108+
const m = mat4.create();
109+
const mInv = mat4.create();
110+
const mInvRef = mat4.create();
111+
for (const {fov, aspect, zNear, zFar} of testParams) {
112+
mat4.perspective(m, fov, aspect, zNear, zFar);
113+
// compute inv:
114+
fastInvertProjMat4(mInv, m);
115+
// compute ref:
116+
mat4.invert(mInvRef, m);
117+
compare_matrix(mInv, mInvRef);
118+
};
119+
});
120+
121+
test('invert a skew matrix', () => {
122+
const testParams = [{
123+
diag: [1, 1, 1, 1],
124+
counterDiagXY: [0, 0]
125+
},{
126+
diag: [2, 3, 4, 5],
127+
counterDiagXY: [0.5, -0.5]
128+
},{
129+
diag: [0.1, 0.2, 0.3, 0.4],
130+
counterDiagXY: [0.01, -0.01]
131+
},{
132+
diag: [10, 10, 10, 1],
133+
counterDiagXY: [3, -3]
134+
},{
135+
diag: [1, 1, 1, 1],
136+
counterDiagXY: [0.9, -0.9]
137+
},{
138+
diag: [100, 200, 50, 1],
139+
counterDiagXY: [10, 20]
140+
},{
141+
diag: [0.5, 0.5, 0.5, 2],
142+
counterDiagXY: [0, 0]
143+
},{
144+
diag: [7, 3, 5, 1],
145+
counterDiagXY: [-2, 1]
146+
},{
147+
diag: [1, 1, 0.01, 1],
148+
counterDiagXY: [0.5, 0.3]
149+
},{
150+
diag: [4, 8, 2, 1],
151+
counterDiagXY: [-5, 3]
152+
},{
153+
diag: [0.3, 0.7, 1.5, 1],
154+
counterDiagXY: [0.1, -0.2]
155+
}];
156+
const m = mat4.create();
157+
const mInv = mat4.create();
158+
const mInvRef = mat4.create();
159+
for(const {diag, counterDiagXY} of testParams) {
160+
// forge a skew matrix:
161+
m[0] = diag[0];
162+
m[5] = diag[1];
163+
m[10] = diag[2];
164+
m[15] = diag[3];
165+
m[4] = counterDiagXY[0];
166+
m[1] = counterDiagXY[1];
167+
168+
// compute inv:
169+
fastInvertSkewMat4(mInv, m);
170+
171+
// compute ref:
172+
mat4.invert(mInvRef, m);
173+
174+
compare_matrix(mInv, mInvRef);
175+
};
176+
});
177+
});

src/util/fast_maths.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
This class implements fast mathematical operations to optimize or complement gl-matrix
3+
*/
4+
import type {mat4} from 'gl-matrix';
5+
6+
/*
7+
Invert a transform matrix
8+
dst and src are in column-major flat format
9+
*/
10+
export function fastInvertTransformMat4(dst: mat4, src: mat4): mat4{
11+
// extract scale. hyp: scale X = scale Y:
12+
const sXYSqInv = 1.0 / (src[0] * src[0] + src[1] * src[1] + src[2] * src[2]);
13+
const sZSqInv = 1.0 / (src[8] * src[8] + src[9] * src[9] + src[10] * src[10]);
14+
15+
// inv rotation and scaling part:
16+
const s0 = src[0] * sXYSqInv;
17+
const s4 = src[4] * sXYSqInv;
18+
const s8 = src[8] * sZSqInv;
19+
const s1 = src[1] * sXYSqInv;
20+
const s5 = src[5] * sXYSqInv;
21+
const s9 = src[9] * sZSqInv;
22+
const s2 = src[2] * sXYSqInv;
23+
const s6 = src[6] * sXYSqInv;
24+
const s10 = src[10] * sZSqInv;
25+
26+
// rotation part:
27+
dst[0] = s0;
28+
dst[1] = s4;
29+
dst[2] = s8;
30+
dst[4] = s1;
31+
dst[5] = s5;
32+
dst[6] = s9;
33+
dst[8] = s2;
34+
dst[9] = s6;
35+
dst[10] = s10;
36+
37+
// translation part:
38+
const t0 = src[12];
39+
const t1 = src[13];
40+
const t2 = src[14];
41+
dst[12] = -s0 * t0 - s1 * t1 - s2 * t2;
42+
dst[13] = -s4 * t0 - s5 * t1 - s6 * t2;
43+
dst[14] = -s8 * t0 - s9 * t1 - s10 * t2;
44+
45+
// constant part:
46+
dst[3] = 0;
47+
dst[7] = 0;
48+
dst[11] = 0;
49+
dst[15] = 1;
50+
51+
return dst;
52+
}
53+
54+
/*
55+
Invert a perspective projection matrix
56+
dst and src are in column-major flat format
57+
*/
58+
export function fastInvertProjMat4(dst: mat4, src: mat4): mat4{
59+
dst[0] = 1 / src[0];
60+
dst[1] = 0;
61+
dst[2] = 0;
62+
dst[3] = 0;
63+
64+
dst[4] = 0;
65+
dst[5] = 1 / src[5];
66+
dst[6] = 0;
67+
dst[7] = 0;
68+
69+
dst[8] = 0;
70+
dst[9] = 0;
71+
dst[10] = 0;
72+
dst[11] = 1 / src[14];
73+
74+
dst[12] = 0;
75+
dst[13] = 0;
76+
dst[14] = -1;
77+
dst[15] = src[10] / src[14];
78+
79+
return dst;
80+
}
81+
82+
/*
83+
Invert a skew matrix
84+
with diag coefficients > 0 and XY counterdiag
85+
dst and src are in column-major flat format
86+
*/
87+
88+
export function fastInvertSkewMat4(dst: mat4, src: mat4): mat4{
89+
const invDetXY = 1.0 / (src[0]*src[5] - src[1]*src[4]);
90+
dst[0] = src[5] * invDetXY;
91+
dst[1] = -src[1] * invDetXY;
92+
dst[2] = 0;
93+
dst[3] = 0;
94+
95+
dst[4] = -src[4] * invDetXY;
96+
dst[5] = src[0] * invDetXY;
97+
dst[6] = 0;
98+
dst[7] = 0;
99+
100+
dst[8] = 0;
101+
dst[9] = 0;
102+
dst[10] = 1 / src[10];
103+
dst[11] = 0;
104+
105+
dst[12] = 0;
106+
dst[13] = 0;
107+
dst[14] = 0;
108+
dst[15] = 1 / src[15];
109+
110+
return dst;
111+
}

src/webgl/draw/draw_color_relief.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function drawColorRelief(painter: Painter, tileManager: TileManager, laye
3737
}
3838
}
3939

40+
let textureMaxSize = 0;
4041
function renderColorRelief(
4142
painter: Painter,
4243
tileManager: TileManager,
@@ -64,7 +65,9 @@ function renderColorRelief(
6465
const tile = tileManager.getTile(coord);
6566
const dem = tile.dem;
6667
if(firstTile) {
67-
const maxLength = gl.getParameter(gl.MAX_TEXTURE_SIZE);
68+
// we should avoid calling gl.getParameter at runtime (GPU stall risk)
69+
textureMaxSize = textureMaxSize || gl.getParameter(gl.MAX_TEXTURE_SIZE);
70+
const maxLength = textureMaxSize;
6871
const {elevationTexture, colorTexture} = layer.getColorRampTextures(context, maxLength, dem.getUnpackVector());
6972
context.activeTexture.set(gl.TEXTURE1);
7073
elevationTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);

src/webgl/draw/draw_symbol.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {StencilMode} from '../stencil_mode';
99
import {DepthMode} from '../depth_mode';
1010
import {CullFaceMode} from '../cull_face_mode';
1111
import {addDynamicAttributes} from '../../data/bucket/symbol_bucket';
12-
12+
import {fastInvertTransformMat4} from '../../util/fast_maths';
1313
import {getAnchorAlignment, WritingMode} from '../../symbol/shaping';
1414
import ONE_EM from '../../symbol/one_em';
1515

@@ -304,7 +304,7 @@ function drawLayerSymbols(
304304
pitchAlignment: SymbolLayerSpecification['layout']['text-pitch-alignment'],
305305
keepUpright: boolean,
306306
stencilMode: StencilMode,
307-
colorMode: Readonly<ColorMode>,
307+
colorMode: Readonly<ColorMode>,
308308
isRenderingToTexture: boolean) {
309309

310310
const context = painter.context;
@@ -376,8 +376,6 @@ function drawLayerSymbols(
376376
// See the comment at the beginning of src/symbol/projection.ts for an overview of the symbol projection process
377377
const s = pixelsToTileUnits(tile, 1, painter.transform.zoom);
378378
const pitchedLabelPlaneMatrix = getPitchedLabelPlaneMatrix(rotateWithMap, painter.transform, s);
379-
const pitchedLabelPlaneMatrixInverse = mat4.create();
380-
mat4.invert(pitchedLabelPlaneMatrixInverse, pitchedLabelPlaneMatrix);
381379
const glCoordMatrixForShader = getGlCoordMatrix(pitchWithMap, rotateWithMap, painter.transform, s);
382380

383381
const translation = translatePosition(transform, tile, translate, translateAnchor);
@@ -389,6 +387,9 @@ function drawLayerSymbols(
389387
bucket.hasIconData();
390388

391389
if (alongLine) {
390+
const pitchedLabelPlaneMatrixInverse = mat4.create();
391+
fastInvertTransformMat4(pitchedLabelPlaneMatrixInverse, pitchedLabelPlaneMatrix);
392+
392393
const getElevation = painter.style.map.terrain ? (x: number, y: number) => painter.style.map.terrain.getElevation(coord, x, y) : null;
393394
const rotateToLine = layer.layout.get('text-rotation-alignment') === 'map';
394395
updateLineLabels(bucket, painter, isText, pitchedLabelPlaneMatrix, pitchedLabelPlaneMatrixInverse, pitchWithMap, keepUpright, rotateToLine, coord.toUnwrapped(), transform.width, transform.height, translation, getElevation);

0 commit comments

Comments
 (0)