@@ -4,50 +4,75 @@ import {register} from '../../util/web_worker_transfer';
44import 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 */
2354export const HEADER_DATA_DRIVEN_MASK = 0 ;
24- export const HEADER_ZOOM_DEPENDENT_MASK = 1 ;
55+ export const HEADER_DZR_MASK = 1 ;
2556export const HEADER_BLOCK_SIZE_VEC4 = 2 ;
2657export 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 */
5883export 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