Skip to content

Commit 1261da7

Browse files
ibesoragithub-actions[bot]
authored andcommitted
Disable viewport-y sorting when multiple segments are used
GitOrigin-RevId: f63be02425d8e82c8b41a56bbc4e2eb90a176641
1 parent 9b84e71 commit 1261da7

2 files changed

Lines changed: 65 additions & 167 deletions

File tree

src/data/bucket/symbol_bucket.ts

Lines changed: 4 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,6 @@ class SymbolBucket implements Bucket {
665665
sortedAngle: number;
666666
featureSortOrder: Array<number>;
667667

668-
// Cache for per-segment sorting optimization (symbols never change batches)
669-
symbolsByBatch: Map<number, Array<{index: number; rotatedY: number; featureIndex: number}>> | null;
670-
671668
collisionCircleArray: Array<number>;
672669
placementInvProjMatrix: mat4;
673670
placementViewportMatrix: mat4;
@@ -2246,10 +2243,10 @@ class SymbolBucket implements Bucket {
22462243
// The index array buffer is rewritten to reference the (unchanged) vertices in the
22472244
// sorted order.
22482245

2249-
// With multiple segments (from UBO batching), sort within each segment independently
2250-
const hasMultipleSegments = this.text.segments.get().length > 1 || this.icon.segments.get().length > 1;
2251-
if (hasMultipleSegments) {
2252-
this.sortFeaturesWithinSegments();
2246+
// Viewport-y sorting is not supported when symbols span multiple render segments.
2247+
// Multiple segments occur when a tile has enough symbols to overflow MAX_VERTEX_ARRAY_LENGTH.
2248+
if (this.text.segments.get().length > 1 || this.icon.segments.get().length > 1) {
2249+
this.sortFeaturesByY = false;
22532250
return;
22542251
}
22552252

@@ -2286,166 +2283,6 @@ class SymbolBucket implements Bucket {
22862283
if (this.icon.indexBuffer) this.icon.indexBuffer.updateData(this.icon.indexArray);
22872284
}
22882285

2289-
/**
2290-
* Get or create the symbols-by-batch grouping with caching.
2291-
* Symbols are grouped by UBO batch and sorted by viewport Y position.
2292-
*/
2293-
private getOrCreateSortedSymbolsByBatch(sin: number, cos: number): Map<number, Array<{index: number; rotatedY: number; featureIndex: number}>> {
2294-
const activeBinder = (this.text.uboBinder && this.text.uboBinder.maxFeaturesPerBatch > 0) ?
2295-
this.text.uboBinder :
2296-
(this.icon.uboBinder && this.icon.uboBinder.maxFeaturesPerBatch > 0) ?
2297-
this.icon.uboBinder : null;
2298-
assert(activeBinder, 'sortFeaturesWithinSegments requires a valid UBO binder with maxFeaturesPerBatch > 0');
2299-
const batchSize = activeBinder.maxFeaturesPerBatch;
2300-
2301-
if (!this.symbolsByBatch) {
2302-
// First time - create and cache the batch grouping
2303-
this.symbolsByBatch = new Map();
2304-
2305-
for (let i = 0; i < this.symbolInstances.length; i++) {
2306-
const symbol = this.symbolInstances.get(i);
2307-
const batchIndex = Math.floor(i / batchSize);
2308-
const rotatedY = Math.round(sin * symbol.tileAnchorX + cos * symbol.tileAnchorY) | 0;
2309-
2310-
if (!this.symbolsByBatch.has(batchIndex)) {
2311-
this.symbolsByBatch.set(batchIndex, []);
2312-
}
2313-
this.symbolsByBatch.get(batchIndex).push({
2314-
index: i,
2315-
rotatedY,
2316-
featureIndex: symbol.featureIndex
2317-
});
2318-
}
2319-
} else {
2320-
// Already grouped - just update rotatedY values for the new angle
2321-
for (const batch of this.symbolsByBatch.values()) {
2322-
for (const item of batch) {
2323-
const symbol = this.symbolInstances.get(item.index);
2324-
item.rotatedY = Math.round(sin * symbol.tileAnchorX + cos * symbol.tileAnchorY) | 0;
2325-
}
2326-
}
2327-
}
2328-
2329-
// Sort each batch by viewport Y position
2330-
for (const batch of this.symbolsByBatch.values()) {
2331-
batch.sort((a, b) => {
2332-
return (a.rotatedY - b.rotatedY) || (b.featureIndex - a.featureIndex);
2333-
});
2334-
}
2335-
2336-
return this.symbolsByBatch;
2337-
}
2338-
2339-
/**
2340-
* Rebuild text segment indices in sorted order within each segment.
2341-
*/
2342-
private rebuildTextSegmentIndices(
2343-
textSegments: Array<Segment>,
2344-
symbolsByBatch: Map<number, Array<{index: number; rotatedY: number; featureIndex: number}>>
2345-
) {
2346-
for (const textSegment of textSegments) {
2347-
const batchIndex = textSegment.batchIndex;
2348-
if (batchIndex === undefined) continue;
2349-
2350-
const batchSymbols = symbolsByBatch.get(batchIndex);
2351-
if (!batchSymbols) {
2352-
textSegment.primitiveOffset = this.text.indexArray.length;
2353-
textSegment.primitiveLength = 0;
2354-
continue;
2355-
}
2356-
2357-
textSegment.primitiveOffset = this.text.indexArray.length;
2358-
const startLength = this.text.indexArray.length;
2359-
2360-
for (const {index: symbolIndex} of batchSymbols) {
2361-
const symbol = this.symbolInstances.get(symbolIndex);
2362-
const {
2363-
rightJustifiedTextSymbolIndex: right,
2364-
centerJustifiedTextSymbolIndex: center,
2365-
leftJustifiedTextSymbolIndex: left,
2366-
verticalPlacedTextSymbolIndex: vertical
2367-
} = symbol;
2368-
2369-
if (right >= 0) this.addIndicesForPlacedSymbol(this.text, right);
2370-
if (center >= 0 && center !== right) this.addIndicesForPlacedSymbol(this.text, center);
2371-
if (left >= 0 && left !== center && left !== right) this.addIndicesForPlacedSymbol(this.text, left);
2372-
if (vertical >= 0) this.addIndicesForPlacedSymbol(this.text, vertical);
2373-
}
2374-
2375-
textSegment.primitiveLength = this.text.indexArray.length - startLength;
2376-
}
2377-
}
2378-
2379-
/**
2380-
* Rebuild icon segment indices in sorted order within each segment.
2381-
*/
2382-
private rebuildIconSegmentIndices(
2383-
iconSegments: Array<Segment>,
2384-
symbolsByBatch: Map<number, Array<{index: number; rotatedY: number; featureIndex: number}>>
2385-
) {
2386-
for (const iconSegment of iconSegments) {
2387-
const batchIndex = iconSegment.batchIndex;
2388-
if (batchIndex === undefined) continue;
2389-
2390-
const batchSymbols = symbolsByBatch.get(batchIndex);
2391-
if (!batchSymbols) {
2392-
iconSegment.primitiveOffset = this.icon.indexArray.length;
2393-
iconSegment.primitiveLength = 0;
2394-
continue;
2395-
}
2396-
2397-
iconSegment.primitiveOffset = this.icon.indexArray.length;
2398-
const startLength = this.icon.indexArray.length;
2399-
2400-
for (const {index: symbolIndex} of batchSymbols) {
2401-
const symbol = this.symbolInstances.get(symbolIndex);
2402-
const {
2403-
placedIconSymbolIndex: icon,
2404-
verticalPlacedIconSymbolIndex: iconVertical
2405-
} = symbol;
2406-
2407-
if (icon >= 0) this.addIndicesForPlacedSymbol(this.icon, icon);
2408-
if (iconVertical >= 0) this.addIndicesForPlacedSymbol(this.icon, iconVertical);
2409-
}
2410-
2411-
iconSegment.primitiveLength = this.icon.indexArray.length - startLength;
2412-
}
2413-
}
2414-
2415-
/**
2416-
* Sort features within each segment independently.
2417-
* Each segment corresponds to a UBO batch - we rebuild index arrays per-segment.
2418-
*/
2419-
sortFeaturesWithinSegments() {
2420-
this.featureSortOrder = [];
2421-
2422-
const sin = Math.sin(this.sortedAngle);
2423-
const cos = Math.cos(this.sortedAngle);
2424-
2425-
// Get sorted symbols grouped by batch (cached for performance)
2426-
const symbolsByBatch = this.getOrCreateSortedSymbolsByBatch(sin, cos);
2427-
2428-
// Build featureSortOrder in batch order
2429-
const sortedBatches = Array.from(symbolsByBatch.keys()).sort((a, b) => a - b);
2430-
for (const batchIndex of sortedBatches) {
2431-
const batchSymbols = symbolsByBatch.get(batchIndex);
2432-
for (const {index: symbolIndex} of batchSymbols) {
2433-
const symbol = this.symbolInstances.get(symbolIndex);
2434-
this.featureSortOrder.push(symbol.featureIndex);
2435-
}
2436-
}
2437-
2438-
// Clear and rebuild index arrays with sorted symbols per segment
2439-
this.text.indexArray.clear();
2440-
this.icon.indexArray.clear();
2441-
2442-
this.rebuildTextSegmentIndices(this.text.segments.get(), symbolsByBatch);
2443-
this.rebuildIconSegmentIndices(this.icon.segments.get(), symbolsByBatch);
2444-
2445-
if (this.text.indexBuffer) this.text.indexBuffer.updateData(this.text.indexArray);
2446-
if (this.icon.indexBuffer) this.icon.indexBuffer.updateData(this.icon.indexArray);
2447-
}
2448-
24492286
}
24502287

24512288
register(SymbolBucket, 'SymbolBucket', {

test/unit/data/symbol_bucket.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {createSymbolBucket} from '../../util/create_symbol_layer';
1616
import {getProjection} from '../../../src/geo/projection/index';
1717
import vectorStub from '../../fixtures/mbsv5-6-18-23.vector.pbf?arraybuffer';
1818
import glyphData from '../../fixtures/fontstack-glyphs.json';
19+
import SegmentVector from '../../../src/data/segment';
20+
import SymbolBucket from '../../../src/data/bucket/symbol_bucket';
21+
import SymbolStyleLayer from '../../../src/style/style_layer/symbol_style_layer';
22+
import featureFilter from '../../../src/style-spec/feature_filter/index';
1923

2024
import type CollisionIndex from '../../../src/symbol/collision_index';
2125
import type {BucketPart} from '../../../src/symbol/placement';
@@ -146,3 +150,60 @@ test('SymbolBucket detects rtl text mixed with ltr text', () => {
146150
expect(mixedBucket.hasRTLText).toBeTruthy();
147151
});
148152

153+
test('sortFeatures multi-segment: disables sorting when multiple segments exist', () => {
154+
// When a tile has enough symbols to overflow MAX_VERTEX_ARRAY_LENGTH, multiple render
155+
// segments are created. Viewport-y sorting across segment boundaries is unsupported
156+
// so sortFeatures must disable sorting by setting sortFeaturesByY to false and returning early
157+
const projection = getProjection({name: 'mercator'});
158+
const originalMax = SegmentVector.MAX_VERTEX_ARRAY_LENGTH;
159+
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 20;
160+
161+
try {
162+
const layer = new SymbolStyleLayer({
163+
id: 'test-multi-seg',
164+
type: 'symbol',
165+
layout: {
166+
'text-font': ['Test'],
167+
'text-field': 'abcdefghij',
168+
'symbol-z-order': 'viewport-y',
169+
'text-allow-overlap': true
170+
},
171+
filter: featureFilter()
172+
}, '');
173+
layer.recalculate({zoom: 0});
174+
175+
const collisionBoxArrayLocal = new CollisionBoxArray();
176+
const bucket = new SymbolBucket({
177+
overscaling: 1,
178+
zoom: 0,
179+
collisionBoxArray: collisionBoxArrayLocal,
180+
layers: [layer],
181+
projection: {name: 'mercator'}
182+
});
183+
184+
bucket.populate([{feature}, {feature}, {feature}, {feature}, {feature}], {iconDependencies: {}, glyphDependencies: {}});
185+
186+
const glyphMap = {'Test': {}};
187+
for (const id in glyphData.glyphs) {
188+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
189+
glyphMap['Test'][id] = glyphData.glyphs[id].rect;
190+
}
191+
192+
const bucketData = performSymbolLayout(bucket, {'Test': glyphData}, glyphMap, null, null, null, null, null, null, projection);
193+
postRasterizationSymbolLayout(bucket, bucketData, null, null, null, null, projection, null, null, {});
194+
195+
// Verify precondition: sortFeaturesByY must be true before calling sortFeatures.
196+
expect(bucket.sortFeaturesByY).toBe(true);
197+
198+
bucket.sortFeatures(Math.PI / 4);
199+
200+
// Multiple segments must have been created by the reduced MAX_VERTEX_ARRAY_LENGTH.
201+
expect(bucket.text.segments.get().length).toBeGreaterThan(1);
202+
203+
// The guard must have disabled sorting to prevent cross-segment index corruption.
204+
expect(bucket.sortFeaturesByY).toBe(false);
205+
} finally {
206+
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = originalMax;
207+
}
208+
});
209+

0 commit comments

Comments
 (0)