diff --git a/src/source/tile.ts b/src/source/tile.ts index 0f29cc2b594..2612d8512fa 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -797,8 +797,9 @@ class Tile { const imagePositions: SpritePositions = this.imageAtlas ? Object.fromEntries(this.imageAtlas.patternPositions) : {}; const withStateUpdates = Object.keys(sourceLayerStates).length > 0 && !isBrightnessChanged; bucket.hasAppearances = bucket.layers.some(layer => layer.appearances && layer.appearances.length > 0); - const layers = withStateUpdates ? bucket.stateDependentLayers : bucket.layers; - if ((withStateUpdates && bucket.stateDependentLayers.length !== 0) || isBrightnessChanged || hasPaintUpdate || needsSymbolUBOUpdate) { + const hasStateDependentLayers = withStateUpdates && bucket.stateDependentLayers.length !== 0; + const layers = hasStateDependentLayers ? bucket.stateDependentLayers : bucket.layers; + if (hasStateDependentLayers || isBrightnessChanged || hasPaintUpdate || needsSymbolUBOUpdate) { const vtLayers = this.latestFeatureIndex.loadVTLayers(); const sourceLayer = vtLayers[sourceLayerId]; bucket.update(sourceLayerStates, sourceLayer, images, imagePositions, layers, isBrightnessChanged, brightness, this.tileID.canonical); @@ -849,15 +850,14 @@ class Tile { } } } - if ((withStateUpdates && bucket.stateDependentLayers.length !== 0) || isBrightnessChanged || bucket.hasAppearances) { + if (hasStateDependentLayers || isBrightnessChanged || bucket.hasAppearances) { const globalProperties = { zoom: painter.transform.zoom, pitch: painter.transform.pitch, brightness: painter.style.getBrightness() || 0, worldview: painter.worldview }; - const featureStateChanged = withStateUpdates && bucket.stateDependentLayers.length !== 0; - const result = bucket.updateAppearances(this.tileID.canonical, sourceLayerStates, images, globalProperties, painter.imageManager, featureStateChanged); + const result = bucket.updateAppearances(this.tileID.canonical, sourceLayerStates, images, globalProperties, painter.imageManager, hasStateDependentLayers); if (result && result.hasUboChanges) { const context = painter.context; if (bucket instanceof SymbolBucket && bucket.text && bucket.text.uboBinder) { diff --git a/test/unit/source/tile.test.ts b/test/unit/source/tile.test.ts index f92e3dc411e..4258ddf711b 100644 --- a/test/unit/source/tile.test.ts +++ b/test/unit/source/tile.test.ts @@ -284,6 +284,44 @@ describe('rtl text detection', () => { ); }); +describe('Tile#updateBuckets', () => { + test('does not throw when feature state and a paint update land on a symbol layer with no state-dependent paint', () => { + const tile = new Tile(new OverscaledTileID(1, 0, 1, 1, 1), 512, 22); + const symbolBucket = createSymbolBucket('test', 'Test', 'test', new CollisionBoxArray()); + // Force UBO binder creation: the crash only happens on the UBO-based update path in + // SymbolBucket#update, which is skipped entirely when text/icon.uboBinder is null. + symbolBucket.createArrays(); + + const layer = symbolBucket.layers[0]; + const painter = createPainter({ + hasLayer: () => true, + listImages: () => [], + getBrightness: () => 0, + getLayerSourceCache: () => undefined, + getLayer: () => layer, + getOwnLayer: () => layer + }); + + tile.loadVectorData( + createVectorData({rawTileData: rawTileData as ArrayBuffer, buckets: [symbolBucket]}), + painter + ); + + // createSymbolBucket() gives the layer only layout properties (text-font, text-field); + // its paint is left at defaults, so it never reads feature-state and stateDependentLayers + // stays empty even though the source below carries feature state. + expect(tile.buckets[layer.fqid].stateDependentLayers).toEqual([]); + + expect(() => tile.updateBuckets( + painter, + false, // isBrightnessChanged + {_geojsonTileLayer: {'1': {hover: true}}}, // states -> withStateUpdates = true + false, // needsSymbolUBOUpdate + new Set([layer.fqid]) // updatedPaintProps -> hasPaintUpdate = true + )).not.toThrow(); + }); +}); + function createVectorData(options?: {buckets?: Bucket[]; rawTileData?: ArrayBuffer}): WorkerSourceVectorTileResult { const collisionBoxArray = new CollisionBoxArray(); return ({collisionBoxArray: deserialize(serialize(collisionBoxArray)),