Skip to content

Commit 1a28fb7

Browse files
underootgithub-actions[bot]
authored andcommitted
Simplify symbol layer UBO handling
GitOrigin-RevId: 1eea89aebaa0c15d4cce67550c0862735d7e7a94
1 parent 916c543 commit 1a28fb7

10 files changed

Lines changed: 408 additions & 445 deletions

File tree

src/data/bucket/symbol_properties_ubo.ts

Lines changed: 92 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,75 @@ import {register} from '../../util/web_worker_transfer';
44
import type Context from '../../gl/context';
55

66
/**
7-
* The UBO layout header is a flat Uint32Array of 12 dwords (3 uvec4), built once per layer by
7+
* The UBO layout header is a flat Uint32Array of 16 dwords (4 uvec4), built once per layer by
88
* SymbolPropertyBinderUBO.updateHeader() and uploaded to the GPU verbatim (matching GL Native and
9-
* the shader's SymbolPropertyHeader struct). These constants name its dword slots:
9+
* the shader's header UBO). These constants name its dword slots:
1010
*
1111
* [HEADER_DATA_DRIVEN_MASK] bitmask: 1 = property goes in the per-feature data-driven block
12-
* [HEADER_ZOOM_DEPENDENT_MASK] low 16 bits: 1 = property uses zoom interpolation (composite kind).
13-
* high 16 bits (>> HEADER_APPEARANCE_ZOOM_STOPS_SHIFT): 1 = appearances
14-
* override this property with differing zoom stops, so its zoom range
15-
* [zm, zM] is stored per feature in the data-driven block
12+
* [HEADER_DZR_MASK] bitmask (bit i per property): 1 = this property's own block slot
13+
* carries a per-feature zoom range [zm, zM], rather than sharing one
14+
* across the layer and read from HEADER_SHARED_ZOOM — and the block
15+
* is 2 vec4 instead of 1. For colors this means appearances override
16+
* the property with differing zoom stops (DifferentZoomRanges); for
17+
* translate — which has no HEADER_SHARED_ZOOM slot to share — this
18+
* means the property is zoom-dependent at all (SameZoomRange or
19+
* DifferentZoomRanges). The shader reads bits 0/1 (fill_color/
20+
* halo_color) to pick the zoom-range source branchlessly and bit 8
21+
* (translate) to pick whether to mix at all; the bit doesn't
22+
* otherwise affect block sizing/offsets beyond the 1-vs-2 vec4
23+
* split above. The broader Independent/SameZoomRange/
24+
* DifferentZoomRanges classification each property needs on the CPU
25+
* lives in SymbolPropertyBinderUBO.zoomDependency, not here —
26+
* matching GL Native, whose header likewise carries only the DZR
27+
* bits the shader consumes.
1628
* [HEADER_BLOCK_SIZE_VEC4] size of the data-driven block in vec4 units (0 when no DD props)
17-
* [HEADER_OFFSETS + i] dword offset of property i within the data-driven block
29+
* [HEADER_OFFSETS + i] vec4-unit offset of property i within the data-driven block
1830
* (only meaningful when property i's data-driven bit is set)
31+
* [HEADER_SHARED_ZOOM + 0..3] [fillZm, fillZM, haloZm, haloZM] (as raw float bits, see
32+
* floatToBits) — the shared [zm, zM] zoom range for fill/halo color
33+
* when NOT DifferentZoomRanges (Independent: {0,0}; SameZoomRange:
34+
* the one range shared by every feature). Read by the shader instead
35+
* of a per-feature block slot in that case.
1936
*
2037
* Property order (bit index 0-8): fill_color, halo_color, opacity, halo_width, halo_blur,
2138
* emissive_strength, occlusion_opacity, z_offset, translate.
39+
*
40+
* Every data-driven property occupies a fixed, zoom-ready slot so the shader decode is branchless
41+
* and uniform for constant / zoom-interpolated / appearance-zoom-stops cases alike (one `zoomFactor`
42+
* + one `mix`); non-zoom values simply duplicate min into max:
43+
* float: 1 vec4 [min, max, zm, zM]
44+
* translate (vec2), not zoom-dependent: 1 vec4 [tx, ty, tx, ty] (zoom range read as [0, 0])
45+
* translate (vec2), zoom-dependent: 2 vec4 [tx_min, ty_min, tx_max, ty_max],
46+
* [zm, zM, pad, pad] (translate has no
47+
* HEADER_SHARED_ZOOM slot, so it always carries
48+
* its own zoom range when zoom-dependent)
49+
* color, Independent/SameZoomRange: 1 vec4 [packMin0, packMin1, packMax0, packMax1]
50+
* (zoom range read from HEADER_SHARED_ZOOM)
51+
* color, DifferentZoomRanges: 2 vec4 [packMin0, packMin1, packMax0, packMax1],
52+
* [zm, zM, pad, pad]
2253
*/
2354
export const HEADER_DATA_DRIVEN_MASK = 0;
24-
export const HEADER_ZOOM_DEPENDENT_MASK = 1;
55+
export const HEADER_DZR_MASK = 1;
2556
export const HEADER_BLOCK_SIZE_VEC4 = 2;
2657
export const HEADER_OFFSETS = 3;
27-
28-
// Bit shift for the appearance-zoom-stops flags packed into the high half of the HEADER_ZOOM_DEPENDENT_MASK dword.
29-
export const HEADER_APPEARANCE_ZOOM_STOPS_SHIFT = 16;
30-
31-
/**
32-
* A packed value for writing into the UBO's data-driven properties array.
33-
*
34-
* Colors (property indices 0-1) — always non-premultiplied; the fragment shader premultiplies:
35-
* non-zoom → [packed0, packed1, 0, 0] (2 floats via packUint8ToFloat + 2 padding)
36-
* zoom-dep → [packColor(min)[0], packColor(min)[1], packColor(max)[0], packColor(max)[1]]
37-
* Floats (property indices 2-7):
38-
* non-zoom → single number
39-
* zoom-dep → [min, max]
40-
* Vec2 (property index 8, translate):
41-
* non-zoom → [tx, ty]
42-
* zoom-dep → [tx_min, ty_min, tx_max, ty_max] (vec4-aligned in the data block)
43-
*/
44-
export type PropertyValue = number | [number, number] | [number, number, number, number];
58+
// [fillZm, fillZM, haloZm, haloZM], packed as float bits — see HEADER_SHARED_ZOOM doc above.
59+
export const HEADER_SHARED_ZOOM = 12;
60+
61+
// Scratch buffer for reinterpreting a float's bit pattern as a uint32 (mirrors GLSL's
62+
// floatBitsToUint), so shared zoom ranges can be packed into the uint32 header alongside the
63+
// other integer fields.
64+
const _floatBitsScratchF32 = new Float32Array(1);
65+
const _floatBitsScratchU32 = new Uint32Array(_floatBitsScratchF32.buffer);
66+
export function floatToBits(value: number): number {
67+
_floatBitsScratchF32[0] = value;
68+
return _floatBitsScratchU32[0];
69+
}
4570

4671
/**
4772
* Manages Uniform Buffer Objects (UBOs) for symbol paint properties.
4873
*
4974
* Uses 3 separate GPU buffers per batch aligned with the GL Native UBO layout:
50-
* - Header buffer (SymbolPaintPropertiesHeaderUniform): 3 uvec4 layout descriptor
75+
* - Header buffer (SymbolPaintPropertiesHeaderUniform): 4 uvec4 layout descriptor
5176
* - Properties buffer (SymbolPaintPropertiesUniform): per-feature data-driven blocks
5277
* - Block indices buffer (SymbolPaintPropertiesIndexUniform): feature→block index mapping
5378
*
@@ -56,19 +81,18 @@ export type PropertyValue = number | [number, number] | [number, number, number,
5681
* Constant properties are NOT stored here — they are passed as u_spp_* uniforms.
5782
*/
5883
export class SymbolPropertiesUBO {
59-
static readonly HEADER_DWORDS = 12; // 3 uvec4s (never changes)
60-
static readonly HEADER_BYTES = 48; // HEADER_DWORDS * 4
61-
62-
// Flat evaluation buffer layout — per-property start offset in a Float32Array(EVAL_FLAT_TOTAL).
63-
// fill_color[0..3], halo_color[4..7], opacity[8..9], halo_width[10..11],
64-
// halo_blur[12..13], emissive_strength[14..15], occlusion_opacity[16..17],
65-
// z_offset[18..19], translate[20..23]
66-
// Each property also gets a 2-slot [zm, zM] zoom range starting at EVAL_FLAT_ZOOM_OFFSETS[i]
67-
// ([24,25] … [40,41]); only written for appearance-zoom-stops properties.
68-
// Colors and translate always use 4 slots; scalars always use 2 (second = 0 for non-zoom).
69-
static readonly EVAL_FLAT_OFFSETS: readonly number[] = [0, 4, 8, 10, 12, 14, 16, 18, 20];
70-
static readonly EVAL_FLAT_ZOOM_OFFSETS: readonly number[] = [24, 26, 28, 30, 32, 34, 36, 38, 40];
71-
static readonly EVAL_FLAT_TOTAL = 42;
84+
static readonly HEADER_DWORDS = 16; // 4 uvec4s (never changes)
85+
static readonly HEADER_BYTES = 64; // HEADER_DWORDS * 4
86+
87+
// Flat evaluation buffer layout — per-property start offset in a Float32Array(EVAL_FLAT_TOTAL),
88+
// mirroring the data-driven block's zoom-ready slot shape exactly (see the layout doc above), so
89+
// writeDataDrivenBlock/_copyFromFlat below are unconditional contiguous copies:
90+
// fill_color[0..7], halo_color[8..15] — [value(4), zoomRange(4)] each
91+
// opacity[16..19], halo_width[20..23], halo_blur[24..27], emissive_strength[28..31],
92+
// occlusion_opacity[32..35], z_offset[36..39] — [min, max, zm, zM] each
93+
// translate[40..47] — [value(4), zoomRange(4)]
94+
static readonly EVAL_FLAT_OFFSETS: readonly number[] = [0, 8, 16, 20, 24, 28, 32, 36, 40];
95+
static readonly EVAL_FLAT_TOTAL = 48;
7296

7397
// The block-indices buffer is a pure identity mapping (blockIndices[i] = i): dedup currently
7498
// happens at the vertex-attribute level (duplicate features get the same index written into the
@@ -92,16 +116,17 @@ export class SymbolPropertiesUBO {
92116

93117
propsDwords: number; // dword count for u_properties
94118
totalBytes: number; // byte size of each of properties / block-indices buffers
95-
headerData: Uint32Array; // 12 uint32s (3 uvec4s)
119+
headerData: Uint32Array; // 16 uint32s (4 uvec4s)
96120
propertiesData: Float32Array; // propsDwords floats — data-driven blocks only
97121
headerBuffer: WebGLBuffer | null;
98122
propertiesBuffer: WebGLBuffer | null;
99123
blockIndicesBuffer: WebGLBuffer | null;
100124
batchIndex: number;
101125
context: Context | null;
102126
// Dirty tracking: each flag/range marks data that needs uploading to GPU.
103-
// headerDirty: the header is built once and passed in at construction, so this just
104-
// triggers a single upload, then stays false.
127+
// headerDirty: true after construction (triggers the first upload) and again whenever
128+
// markHeaderDirty() is called — the HEADER_SHARED_ZOOM slots can change at runtime (see
129+
// SymbolPropertyBinderUBO._recomputeSharedRanges), unlike the rest of the header.
105130
// propsDirtyMin/Max: dword range touched by writeDataDrivenBlock; -1 means clean.
106131
// blockIndicesDirty: the shared identity template is uploaded once per batch's GPU buffer,
107132
// so this clears after the first upload and stays false.
@@ -159,6 +184,15 @@ export class SymbolPropertiesUBO {
159184
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
160185
}
161186

187+
/**
188+
* Marks the header buffer for re-upload. Call after mutating `headerData` in place post-
189+
* construction (currently only HEADER_SHARED_ZOOM, refreshed by
190+
* SymbolPropertyBinderUBO._recomputeSharedRanges on runtime paint-property changes).
191+
*/
192+
markHeaderDirty(): void {
193+
this._headerDirty = true;
194+
}
195+
162196
/**
163197
* Write all data-driven properties for one feature from a flat evaluation buffer.
164198
*
@@ -175,11 +209,9 @@ export class SymbolPropertiesUBO {
175209
throw new Error(`UBO write out of bounds: feature index ${featureIndex} exceeds propertiesData capacity`);
176210
}
177211
const dataDrivenMask = h[HEADER_DATA_DRIVEN_MASK];
178-
const zoomDependentMask = h[HEADER_ZOOM_DEPENDENT_MASK];
179-
const appearanceZoomStopsMask = zoomDependentMask >>> HEADER_APPEARANCE_ZOOM_STOPS_SHIFT;
180212
for (let i = 0; i < 9; i++) {
181213
if ((dataDrivenMask & (1 << i)) === 0) continue;
182-
this._copyFromFlat(base + h[HEADER_OFFSETS + i], i, flat, SymbolPropertiesUBO.EVAL_FLAT_OFFSETS[i], zoomDependentMask, appearanceZoomStopsMask);
214+
this._copyFromFlat(base + h[HEADER_OFFSETS + i] * 4, i, flat);
183215
}
184216
// Track dword range touched so upload() can do a partial bufferSubData.
185217
if (this._propsDirtyMin === -1 || base < this._propsDirtyMin) this._propsDirtyMin = base;
@@ -207,29 +239,23 @@ export class SymbolPropertiesUBO {
207239
}
208240

209241
/**
210-
* Copy one property's values from the flat evaluation buffer into propertiesData.
211-
*
212-
* Colors (propIdx < 2) and zoom-dep translate always copy 4 dwords.
213-
* Non-zoom translate and zoom-dep scalars copy 2 dwords. Non-zoom scalars copy 1 dword.
214-
*
215-
* Appearance-zoom-stops properties additionally carry a [zm, zM] pair, written after
216-
* the value data: at relative dwords +4/+5 for colors/translate (the next vec4) and +2/+3 for
217-
* scalars (the third/fourth dword of the value's vec4).
242+
* Copy one property's slot from the flat evaluation buffer into propertiesData. Scalars
243+
* always copy 4 dwords (just the packed value). Colors and translate copy 4 dwords (just the
244+
* value) unless this property's HEADER_DZR_MASK bit is set, where they copy 8 (value vec4 +
245+
* [zm, zM, pad, pad]) — see the HEADER_DZR_MASK doc above. The flat buffer's layout (see
246+
* EVAL_FLAT_OFFSETS) always has room for the full 8, so slicing a smaller prefix when not
247+
* needed is safe.
218248
*/
219-
private _copyFromFlat(dwordOffset: number, propIdx: number, flat: Float32Array, flatOffset: number, zoomDependentMask: number, appearanceZoomStopsMask: number): void {
249+
private _copyFromFlat(dwordOffset: number, propIdx: number, flat: Float32Array): void {
220250
const isColor = propIdx < 2;
221-
const isVec2 = propIdx === 8;
222-
const isZoomDep = (zoomDependentMask & (1 << propIdx)) !== 0;
223-
const count = isColor || (isVec2 && isZoomDep) ? 4 : (isVec2 || isZoomDep) ? 2 : 1;
224-
225-
this.propertiesData.set(flat.subarray(flatOffset, flatOffset + count), dwordOffset);
226-
227-
if ((appearanceZoomStopsMask & (1 << propIdx)) !== 0) {
228-
const zoomRel = isColor || isVec2 ? 4 : 2;
229-
const zStart = SymbolPropertiesUBO.EVAL_FLAT_ZOOM_OFFSETS[propIdx];
230-
this.propertiesData[dwordOffset + zoomRel] = flat[zStart];
231-
this.propertiesData[dwordOffset + zoomRel + 1] = flat[zStart + 1];
251+
const isTranslate = propIdx === 8;
252+
let size = 4;
253+
if (isColor || isTranslate) {
254+
const isDzr = ((this.headerData[HEADER_DZR_MASK] >>> propIdx) & 1) !== 0;
255+
size = isDzr ? 8 : 4;
232256
}
257+
const flatOffset = SymbolPropertiesUBO.EVAL_FLAT_OFFSETS[propIdx];
258+
this.propertiesData.set(flat.subarray(flatOffset, flatOffset + size), dwordOffset);
233259
}
234260

235261
/**

0 commit comments

Comments
 (0)