Skip to content

Commit 23a79e9

Browse files
kylebarronclaude
andcommitted
perf(raster-tileset): memoize tileTransform output per (col, row)
`AffineTilesetLevel.tileTransform` and `TileMatrixAdaptor.tileTransform` each constructed two new arrow functions (`forwardTransform`, `inverseTransform`) on every call. `RasterTileLayer._renderSubLayers` invokes these per tile on every render, so the returned references churned constantly. That instability tripped `reprojectionFnsChanged` in `RasterLayer.updateState`, which re-ran `_generateMesh`, which produced a fresh `state.mesh` wrapper, which tripped `props.mesh !== oldProps.mesh` in `SimpleMeshLayer.updateState`, which destroyed and rebuilt the GPU `Model` — incurring full shader assembly per tile per render. The transforms for a given `(col, row)` are deterministic and the level itself is long-lived. Caching them by key collapses the entire chain: stable transform refs → no spurious mesh regen → no Model rebuild → no shader reassembly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3e05a59 commit 23a79e9

4 files changed

Lines changed: 91 additions & 6 deletions

File tree

packages/deck.gl-raster/src/raster-tileset/affine-tileset-level.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ export class AffineTilesetLevel implements TilesetLevel {
4444

4545
private readonly _affine: Affine;
4646
private readonly _invAffine: Affine;
47+
/**
48+
* Memoized output of {@link tileTransform}. The transforms for a given
49+
* `(col, row)` never change for the life of this level, and consumers
50+
* (e.g. `RasterTileLayer._renderSubLayers`) rely on reference stability
51+
* to avoid spurious mesh regeneration downstream in `RasterLayer`.
52+
*/
53+
private readonly _tileTransformCache = new Map<
54+
string,
55+
{
56+
forwardTransform: ProjectionFunction;
57+
inverseTransform: ProjectionFunction;
58+
}
59+
>();
4760

4861
constructor(options: AffineTilesetLevelOptions) {
4962
this._affine = options.affine;
@@ -94,16 +107,25 @@ export class AffineTilesetLevel implements TilesetLevel {
94107
forwardTransform: ProjectionFunction;
95108
inverseTransform: ProjectionFunction;
96109
} {
110+
const key = `${col}|${row}`;
111+
const cached = this._tileTransformCache.get(key);
112+
if (cached) {
113+
return cached;
114+
}
97115
const tileOffset = affine.translation(
98116
col * this.tileWidth,
99117
row * this.tileHeight,
100118
);
101119
const tileAffine = affine.compose(this._affine, tileOffset);
102120
const invTileAffine = affine.invert(tileAffine);
103-
return {
104-
forwardTransform: (x, y) => affine.apply(tileAffine, x, y),
105-
inverseTransform: (x, y) => affine.apply(invTileAffine, x, y),
121+
const result = {
122+
forwardTransform: (x: number, y: number): [number, number] =>
123+
affine.apply(tileAffine, x, y),
124+
inverseTransform: (x: number, y: number): [number, number] =>
125+
affine.apply(invTileAffine, x, y),
106126
};
127+
this._tileTransformCache.set(key, result);
128+
return result;
107129
}
108130

109131
crsBoundsToTileRange(

packages/deck.gl-raster/src/raster-tileset/tile-matrix-set.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ const SCREEN_PIXEL_SIZE = 0.00028;
1010

1111
class TileMatrixAdaptor implements TilesetLevel {
1212
inner: TileMatrix;
13+
/**
14+
* Memoized output of {@link tileTransform}. The transforms for a given
15+
* `(col, row)` never change for the life of this level, and consumers
16+
* (e.g. `RasterTileLayer._renderSubLayers`) rely on reference stability
17+
* to avoid spurious mesh regeneration downstream in `RasterLayer`.
18+
*/
19+
private readonly _tileTransformCache = new Map<
20+
string,
21+
{
22+
forwardTransform: ProjectionFunction;
23+
inverseTransform: ProjectionFunction;
24+
}
25+
>();
1326

1427
constructor(tileMatrix: TileMatrix) {
1528
this.inner = tileMatrix;
@@ -135,12 +148,21 @@ class TileMatrixAdaptor implements TilesetLevel {
135148
forwardTransform: ProjectionFunction;
136149
inverseTransform: ProjectionFunction;
137150
} {
151+
const key = `${col}|${row}`;
152+
const cached = this._tileTransformCache.get(key);
153+
if (cached) {
154+
return cached;
155+
}
138156
const fwd = tileTransform(this.inner, { col, row });
139157
const inv = affine.invert(fwd);
140-
return {
141-
forwardTransform: (x, y) => affine.apply(fwd, x, y),
142-
inverseTransform: (x, y) => affine.apply(inv, x, y),
158+
const result = {
159+
forwardTransform: (x: number, y: number): [number, number] =>
160+
affine.apply(fwd, x, y),
161+
inverseTransform: (x: number, y: number): [number, number] =>
162+
affine.apply(inv, x, y),
143163
};
164+
this._tileTransformCache.set(key, result);
165+
return result;
144166
}
145167
}
146168

packages/deck.gl-raster/tests/raster-tileset/affine-tileset-level.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,37 @@ describe("AffineTilesetLevel", () => {
194194
expect(range).toEqual({ minCol: 0, maxCol: 0, minRow: 0, maxRow: 0 });
195195
});
196196
});
197+
198+
describe("tileTransform memoization", () => {
199+
it("returns the same reference for the same (col, row)", () => {
200+
const level = new AffineTilesetLevel({
201+
affine: SQUARE_AFFINE,
202+
arrayWidth: 16,
203+
arrayHeight: 16,
204+
tileWidth: 4,
205+
tileHeight: 4,
206+
mpu: 1,
207+
});
208+
const first = level.tileTransform(1, 2);
209+
const second = level.tileTransform(1, 2);
210+
expect(second).toBe(first);
211+
expect(second.forwardTransform).toBe(first.forwardTransform);
212+
expect(second.inverseTransform).toBe(first.inverseTransform);
213+
});
214+
215+
it("returns distinct references for different (col, row)", () => {
216+
const level = new AffineTilesetLevel({
217+
affine: SQUARE_AFFINE,
218+
arrayWidth: 16,
219+
arrayHeight: 16,
220+
tileWidth: 4,
221+
tileHeight: 4,
222+
mpu: 1,
223+
});
224+
const a = level.tileTransform(0, 0);
225+
const b = level.tileTransform(1, 0);
226+
expect(b).not.toBe(a);
227+
expect(b.forwardTransform).not.toBe(a.forwardTransform);
228+
});
229+
});
197230
});

packages/deck.gl-raster/tests/raster-tileset/tile-transform.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,12 @@ describe("TileMatrixSetAdaptor.tileTransform", () => {
5757
expect(px).toBeCloseTo(1.3, 10);
5858
expect(py).toBeCloseTo(0.7, 10);
5959
});
60+
61+
it("returns the same reference for the same (col, row)", () => {
62+
const first = level.tileTransform(0, 0);
63+
const second = level.tileTransform(0, 0);
64+
expect(second).toBe(first);
65+
expect(second.forwardTransform).toBe(first.forwardTransform);
66+
expect(second.inverseTransform).toBe(first.inverseTransform);
67+
});
6068
});

0 commit comments

Comments
 (0)