@@ -55,6 +55,25 @@ export class SymbolPropertiesUBO {
5555 static readonly HEADER_DWORDS = 12 ; // 3 uvec4s (never changes)
5656 static readonly HEADER_BYTES = 48 ; // HEADER_DWORDS * 4
5757
58+ // Flat evaluation buffer layout — per-property start offsets and slot counts in a Float32Array(EVAL_FLAT_TOTAL).
59+ // fill_color[0..3], halo_color[4..7], opacity[8..9], halo_width[10..11],
60+ // halo_blur[12..13], emissive_strength[14..15], occlusion_opacity[16..17],
61+ // z_offset[18..19], translate[20..23].
62+ // Colors and translate always use 4 slots; scalars always use 2 (second = 0 for non-zoom).
63+ static readonly EVAL_FLAT_OFFSETS : readonly number [ ] = [ 0 , 4 , 8 , 10 , 12 , 14 , 16 , 18 , 20 ] ;
64+ static readonly EVAL_FLAT_SIZES : readonly number [ ] = [ 4 , 4 , 2 , 2 , 2 , 2 , 2 , 2 , 4 ] ;
65+ static readonly EVAL_FLAT_TOTAL = 24 ;
66+
67+ // Until we support layout properties in UBOs, blockIndicesData is just
68+ // an identity mapping since deduplication happens at the vertex attribute level
69+ // (duplicate features get the same index written into the vertex buffer, so no
70+ // indirection is needed). When we move layout properties to UBO we'll use this
71+ // array to deduplicate paint properties and add a new array for layout properties
72+ // deduplication.
73+ // Until we need that, we just create the identity-mapping once and create a copy
74+ // of it
75+ private static _blockIndicesTemplate : Uint32Array | null = null ;
76+
5877 propsDwords : number ; // dword count for u_properties
5978 totalBytes : number ; // byte size of each of properties / block-indices buffers
6079 headerData : Uint32Array ; // 12 uint32s (3 uvec4s)
@@ -77,14 +96,13 @@ export class SymbolPropertiesUBO {
7796 this . headerData = new Uint32Array ( SymbolPropertiesUBO . HEADER_DWORDS ) ;
7897 this . propertiesData = new Float32Array ( this . propsDwords ) ;
7998
80- // Block indices are write-once identity mapping: blockIndices[i] = i.
81- // Deduplication works by assigning duplicate features the same a_feature_index
82- // (localFeatureIndex), so the shader maps each index to itself — no indirection needed.
83- // This buffer never changes after construction.
84- this . blockIndicesData = new Uint32Array ( this . propsDwords ) ;
85- for ( let i = 0 ; i < this . propsDwords ; i ++ ) {
86- this . blockIndicesData [ i ] = i ;
99+ if ( ! SymbolPropertiesUBO . _blockIndicesTemplate ) {
100+ SymbolPropertiesUBO . _blockIndicesTemplate = new Uint32Array ( this . propsDwords ) ;
101+ for ( let i = 0 ; i < this . propsDwords ; i ++ ) {
102+ SymbolPropertiesUBO . _blockIndicesTemplate [ i ] = i ;
103+ }
87104 }
105+ this . blockIndicesData = new Uint32Array ( SymbolPropertiesUBO . _blockIndicesTemplate ) ;
88106
89107 if ( context ) {
90108 this . _initBuffers ( context ) ;
@@ -141,12 +159,13 @@ export class SymbolPropertiesUBO {
141159 }
142160
143161 /**
144- * Write all data-driven properties for one feature.
162+ * Write all data-driven properties for one feature from a flat evaluation buffer .
145163 *
146164 * The feature's block starts at dword offset: featureIndex * dataDrivenBlockSizeDwords.
147165 * (No constant block — constant properties are passed as u_spp_* uniforms at draw time.)
166+ * `flat` is a Float32Array(EVAL_FLAT_TOTAL) produced by evaluateAllProperties().
148167 */
149- writeDataDrivenBlock ( values : Array < PropertyValue | null > , featureIndex : number , header : SymbolPropertyHeader ) : void {
168+ writeDataDrivenBlock ( flat : Float32Array , featureIndex : number , header : SymbolPropertyHeader ) : void {
150169 const dataDrivenBlockSizeDwords = header . dataDrivenBlockSizeVec4 * 4 ;
151170 if ( dataDrivenBlockSizeDwords === 0 ) return ;
152171 const base = featureIndex * dataDrivenBlockSizeDwords ;
@@ -155,62 +174,42 @@ export class SymbolPropertiesUBO {
155174 }
156175 for ( let i = 0 ; i < 9 ; i ++ ) {
157176 if ( ( header . dataDrivenMask & ( 1 << i ) ) === 0 ) continue ;
158- if ( values [ i ] === null || values [ i ] === undefined ) continue ;
159- this . _writeProperty ( base + header . offsets [ i ] , i , values [ i ] , header . zoomDependentMask ) ;
177+ this . _copyFromFlat ( base + header . offsets [ i ] , i , flat , SymbolPropertiesUBO . EVAL_FLAT_OFFSETS [ i ] , header . zoomDependentMask ) ;
160178 }
161179 }
162180
163181 /**
164182 * Maximum number of features that fit in one UBO batch given a header.
165183 * Returns Infinity when dataDrivenBlockSizeVec4 is 0 (all properties constant).
166184 */
167- static getMaxFeatureCount ( header : SymbolPropertyHeader , propsDwords : number = 4096 - SymbolPropertiesUBO . HEADER_DWORDS ) : number {
185+ static getMaxFeatureCount ( header : SymbolPropertyHeader , propsDwords : number = 4096 ) : number {
168186 const dataDrivenBlockSizeDwords = header . dataDrivenBlockSizeVec4 * 4 ;
169187 if ( dataDrivenBlockSizeDwords === 0 ) return Infinity ;
170188 return Math . floor ( propsDwords / dataDrivenBlockSizeDwords ) ;
171189 }
172190
173191 /**
174- * Write a single property value at the given dword offset in propertiesData.
192+ * Copy one property's values from the flat evaluation buffer into propertiesData.
175193 *
176- * Colors (propIdx < 2) always occupy 4 dwords — non-premultiplied, packed:
177- * non-zoom → [packed0, packed1, 0, 0]
178- * zoom-dep → [packMin[0], packMin[1], packMax[0], packMax[1]]
179- * Floats occupy 1 dword (non-zoom) or 2 dwords (zoom-dep, [min, max]).
194+ * Colors (propIdx < 2) and zoom-dep translate always copy 4 dwords.
195+ * Non-zoom translate and zoom-dep scalars copy 2 dwords. Non-zoom scalars copy 1 dword.
180196 */
181- private _writeProperty ( dwordOffset : number , propIdx : number , value : PropertyValue , zoomDependentMask : number ) : void {
197+ private _copyFromFlat ( dwordOffset : number , propIdx : number , flat : Float32Array , flatOffset : number , zoomDependentMask : number ) : void {
182198 const pd = this . propertiesData ;
183- // Property order is fixed by the GL Native contract: 0=fill_color, 1=halo_color (colors),
184- // 2-7=floats, 8=translate (vec2).
185- // This must stay in sync with _getPropDefs() in symbol_property_binder_ubo.ts.
186199 const isColor = propIdx < 2 ;
187- const isVec2 = propIdx === 8 ; // translate: [tx, ty] non-zoom or [tx_min, ty_min, tx_max, ty_max] zoom-dep
200+ const isVec2 = propIdx === 8 ;
188201 const isZoomDep = ( zoomDependentMask & ( 1 << propIdx ) ) !== 0 ;
189202
190- if ( isColor ) {
191- const v = value as [ number , number , number , number ] ;
192- pd [ dwordOffset ] = v [ 0 ] ;
193- pd [ dwordOffset + 1 ] = v [ 1 ] ;
194- pd [ dwordOffset + 2 ] = v [ 2 ] ;
195- pd [ dwordOffset + 3 ] = v [ 3 ] ;
196- } else if ( isVec2 && isZoomDep ) {
197- // zoom-dep translate: [tx_min, ty_min, tx_max, ty_max], vec4-aligned
198- const v = value as [ number , number , number , number ] ;
199- pd [ dwordOffset ] = v [ 0 ] ; // tx_min
200- pd [ dwordOffset + 1 ] = v [ 1 ] ; // ty_min
201- pd [ dwordOffset + 2 ] = v [ 2 ] ; // tx_max
202- pd [ dwordOffset + 3 ] = v [ 3 ] ; // ty_max
203- } else if ( isVec2 ) {
204- // non-zoom translate: [tx, ty], 2-aligned (both within same vec4)
205- const v = value as [ number , number ] ;
206- pd [ dwordOffset ] = v [ 0 ] ; // tx
207- pd [ dwordOffset + 1 ] = v [ 1 ] ; // ty
208- } else if ( isZoomDep ) {
209- const v = value as [ number , number ] ;
210- pd [ dwordOffset ] = v [ 0 ] ; // min
211- pd [ dwordOffset + 1 ] = v [ 1 ] ; // max
203+ if ( isColor || ( isVec2 && isZoomDep ) ) {
204+ pd [ dwordOffset ] = flat [ flatOffset ] ;
205+ pd [ dwordOffset + 1 ] = flat [ flatOffset + 1 ] ;
206+ pd [ dwordOffset + 2 ] = flat [ flatOffset + 2 ] ;
207+ pd [ dwordOffset + 3 ] = flat [ flatOffset + 3 ] ;
208+ } else if ( isVec2 || isZoomDep ) {
209+ pd [ dwordOffset ] = flat [ flatOffset ] ;
210+ pd [ dwordOffset + 1 ] = flat [ flatOffset + 1 ] ;
212211 } else {
213- pd [ dwordOffset ] = value as number ;
212+ pd [ dwordOffset ] = flat [ flatOffset ] ;
214213 }
215214 }
216215
0 commit comments