Skip to main content

mlt_core/encoder/
optimizer.rs

1use bitvec::vec::BitVec;
2
3use crate::decoder::{Morton, PropKind, TileLayer};
4use crate::encoder::model::{CurveParams, StagedLayer};
5use crate::encoder::property::encode::write_properties;
6use crate::encoder::{
7    Codecs, Encoder, EncoderConfig, SortStrategy, StagedId, spatial_sort_likely_to_help,
8};
9use crate::{MltError, MltResult, PropValue};
10
11impl StagedLayer {
12    /// Encode and serialize the layer directly into `enc`, without creating any
13    /// intermediate representation.
14    ///
15    /// This is the hot path inside `TileLayer::encode`: each sort-strategy
16    /// trial calls this method on its own fresh `Encoder`, and only the
17    /// `Encoder` with the smallest `total_len()` is kept.
18    #[hotpath::measure]
19    pub fn encode_into(self, mut enc: Encoder, codecs: &mut Codecs) -> MltResult<Encoder> {
20        let column_count = usize::from(!matches!(&self.id, StagedId::None))
21            + 1 // geometry
22            + self.properties.len();
23
24        let Self {
25            name,
26            extent,
27            id,
28            geometry,
29            properties,
30        } = self;
31
32        id.write_to(&mut enc, codecs)?;
33        geometry.write_to(&mut enc, codecs)?;
34        write_properties(&properties, &mut enc, codecs)?;
35        enc.write_header(&name, extent, column_count)?;
36
37        Ok(enc)
38    }
39}
40
41/// Seed the encoder's curve-derived caches so the Hilbert/Morton dictionary
42/// builders skip their min/max scan. `Morton::new` returns `Err` when bits > 16;
43/// `dict_may_be_beneficial` reads `morton_cache.is_none()` and falls back to
44/// a Vec2-only path in that case.
45fn seed_curve_caches(enc: &mut Encoder, curve_params: CurveParams) {
46    enc.hilbert_cache = Some(curve_params);
47    enc.morton_cache = Morton::new(curve_params.bits, curve_params.shift).ok();
48}
49
50/// Feature-count threshold above which the spatial trial is subject to the
51/// bounding-box pruning heuristic.
52const SORT_TRIAL_THRESHOLD: usize = 512;
53
54impl TileLayer {
55    /// Encode a [`TileLayer`] to bytes, automatically optimizing all encoding choices.
56    ///
57    /// This is the primary encoding entry point. It:
58    /// 1. Determines which sort strategies to try based on `cfg`
59    /// 2. Tries each sort strategy, encoding and measuring the output size
60    /// 3. Returns the smallest encoding as a complete layer record (including tag and length prefix)
61    ///
62    /// All encoding choices — sort order, per-stream integer encodings, string compression,
63    /// vertex buffer layout — are selected automatically to minimize output size.
64    #[hotpath::measure]
65    pub fn encode(self, cfg: EncoderConfig) -> MltResult<Vec<u8>> {
66        if self.features.is_empty() {
67            return Ok(Vec::new());
68        }
69
70        let mut sort_by = vec![SortStrategy::Unsorted];
71        let try_spatial_sort = cfg.try_spatial_morton_sort || cfg.try_spatial_hilbert_sort;
72        if try_spatial_sort
73            && (self.features.len() < SORT_TRIAL_THRESHOLD || spatial_sort_likely_to_help(&self))
74        {
75            if cfg.try_spatial_morton_sort {
76                sort_by.push(SortStrategy::SpatialMorton);
77            }
78            if cfg.try_spatial_hilbert_sort {
79                sort_by.push(SortStrategy::SpatialHilbert);
80            }
81        }
82        if cfg.try_id_sort {
83            sort_by.push(SortStrategy::Id);
84        }
85
86        let stats = self.analyze(cfg.allow_shared_dict)?;
87        // Bounds are order-invariant, so this scan is shared across every
88        // sort trial and the encoder's Hilbert/Morton dictionary builders.
89        let curve_params = self.curve_params();
90
91        // `Encoder::preserve_results` clears caches only on the moved-out
92        // archive, so a single seeding here serves every trial that reuses
93        // `enc`.
94        let mut enc = Encoder::new(cfg);
95        seed_curve_caches(&mut enc, curve_params);
96
97        let (last, init) = sort_by.split_last().expect("at least one strategy");
98        if init.is_empty() {
99            let mut codecs = Codecs::default();
100            StagedLayer::from_tile(self, *last, &stats, cfg.tessellate, curve_params)
101                .encode_into(enc, &mut codecs)?
102        } else {
103            let mut codecs = Codecs::default();
104            enc = {
105                let first = init[0];
106                StagedLayer::from_tile(self.clone(), first, &stats, cfg.tessellate, curve_params)
107                    .encode_into(enc, &mut codecs)?
108            };
109            let mut best = enc.preserve_results();
110            // Clone for all-but-last strategies
111            for &sort in &init[1..] {
112                let layer = StagedLayer::from_tile(
113                    self.clone(),
114                    sort,
115                    &stats,
116                    cfg.tessellate,
117                    curve_params,
118                );
119                enc = layer.encode_into(enc, &mut codecs)?;
120                if enc.total_len() < best.total_len() {
121                    best = enc.preserve_results();
122                }
123            }
124            // Last strategy: consume self, no clone
125            let layer = StagedLayer::from_tile(self, *last, &stats, cfg.tessellate, curve_params);
126            enc = layer.encode_into(enc, &mut codecs)?;
127            if enc.total_len() < best.total_len() {
128                best = enc.preserve_results();
129            }
130            best
131        }
132        .into_layer_bytes()
133    }
134}
135
136/// Row-order-independent presence classification for IDs and properties.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum Presence {
139    /// No feature has a value for this logical column.
140    AllNull,
141    /// Every feature has a value for this logical column.
142    AllPresent,
143    /// Some, but not all, features have a value for this logical column.
144    Mixed,
145    /// Mixed presence with the same per-feature mask as an earlier property column.
146    SameAsProp(usize),
147}
148impl Presence {
149    /// Create presence value
150    #[must_use]
151    pub fn from_bits(bits: &BitVec<u8>, existing: &[(BitVec<u8>, usize)]) -> Self {
152        if bits.not_any() {
153            Self::AllNull
154        } else if bits.all() {
155            Self::AllPresent
156        } else if let Some((_, idx)) = existing.iter().find(|(v, _)| v == bits) {
157            Self::SameAsProp(*idx)
158        } else {
159            Self::Mixed
160        }
161    }
162}
163
164/// How a property participates in a shared dictionary group.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum SharedDictRole {
167    /// The property is encoded as a standalone column.
168    None,
169    /// The property is the first column in this group and emits the shared dictionary with this prefix.
170    Owner(String),
171    /// The property is emitted by the group owner at this property index.
172    Member(usize),
173}
174
175/// Row-order-independent facts for a single property column.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct PropertyStats {
178    pub presence: Presence,
179    pub stats: PropertyTypedStats,
180}
181
182/// Row-order-independent layer facts computed once before sort trials.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct LayerStats {
185    pub id: Option<PropertyStats>,
186    pub properties: Vec<PropertyStats>,
187}
188
189/// Row-order-independent value statistics for a property column.
190#[derive(Debug, Clone, Default, PartialEq, Eq)]
191pub enum PropertyTypedStats {
192    /// No present values.
193    #[default]
194    None,
195    Bool,
196    Signed {
197        min: i64,
198        max: i64,
199    },
200    Unsigned {
201        min: u64,
202        max: u64,
203    },
204    F32,
205    F64,
206    String {
207        shared_dict: SharedDictRole,
208    },
209}
210
211impl PropertyTypedStats {
212    #[must_use]
213    pub fn values_fit_u32(&self) -> bool {
214        match self {
215            Self::None | Self::Bool | Self::F32 | Self::F64 | Self::String { .. } => false,
216            Self::Signed { min, max } => *min >= 0 && u32::try_from(*max).is_ok(),
217            Self::Unsigned { max, .. } => u32::try_from(*max).is_ok(),
218        }
219    }
220
221    #[must_use]
222    pub fn shared_dict(&self) -> SharedDictRole {
223        match self {
224            Self::String { shared_dict, .. } => shared_dict.clone(),
225            _ => SharedDictRole::None,
226        }
227    }
228
229    pub(crate) fn set_shared_dict(&mut self, role: SharedDictRole) {
230        match self {
231            Self::String { shared_dict, .. } => *shared_dict = role,
232            _ => debug_assert_eq!(role, SharedDictRole::None),
233        }
234    }
235
236    pub(crate) fn push(
237        &mut self,
238        prop: &PropValue,
239        column_idx: usize,
240        property_name: &str,
241    ) -> MltResult<bool> {
242        match prop {
243            PropValue::Bool(Some(_)) => {
244                self.merge_same_kind(Self::Bool, column_idx, property_name)?;
245            }
246            PropValue::I8(Some(v)) => {
247                self.merge_signed(i64::from(*v), column_idx, property_name)?;
248            }
249            PropValue::U8(Some(v)) => {
250                self.merge_unsigned(u64::from(*v), column_idx, property_name)?;
251            }
252            PropValue::I32(Some(v)) => {
253                self.merge_signed(i64::from(*v), column_idx, property_name)?;
254            }
255            PropValue::U32(Some(v)) => {
256                self.merge_unsigned(u64::from(*v), column_idx, property_name)?;
257            }
258            PropValue::I64(Some(v)) => self.merge_signed(*v, column_idx, property_name)?,
259            PropValue::U64(Some(v)) => self.merge_unsigned(*v, column_idx, property_name)?,
260            PropValue::F32(Some(_)) => {
261                self.merge_same_kind(Self::F32, column_idx, property_name)?;
262            }
263            PropValue::F64(Some(_)) => {
264                self.merge_same_kind(Self::F64, column_idx, property_name)?;
265            }
266            PropValue::Str(Some(_)) => self.merge_string(column_idx, property_name)?,
267            _ => return Ok(false),
268        }
269        Ok(true)
270    }
271
272    fn merge_signed(
273        &mut self,
274        value: i64,
275        column_idx: usize,
276        property_name: &str,
277    ) -> MltResult<()> {
278        match self {
279            Self::None => {
280                *self = Self::Signed {
281                    min: value,
282                    max: value,
283                };
284            }
285            Self::Signed { min, max } => {
286                *min = (*min).min(value);
287                *max = (*max).max(value);
288            }
289            _ => return mixed_prop_err(column_idx, property_name),
290        }
291        Ok(())
292    }
293
294    fn merge_unsigned(
295        &mut self,
296        value: u64,
297        column_idx: usize,
298        property_name: &str,
299    ) -> MltResult<()> {
300        match self {
301            Self::None => {
302                *self = Self::Unsigned {
303                    min: value,
304                    max: value,
305                };
306            }
307            Self::Unsigned { min, max } => {
308                *min = (*min).min(value);
309                *max = (*max).max(value);
310            }
311            _ => return mixed_prop_err(column_idx, property_name),
312        }
313        Ok(())
314    }
315
316    fn merge_string(&mut self, column_idx: usize, property_name: &str) -> MltResult<()> {
317        match self {
318            Self::None => {
319                *self = Self::String {
320                    shared_dict: SharedDictRole::None,
321                };
322            }
323            Self::String { .. } => {}
324            _ => return mixed_prop_err(column_idx, property_name),
325        }
326        Ok(())
327    }
328
329    fn merge_same_kind(
330        &mut self,
331        kind: Self,
332        column_idx: usize,
333        property_name: &str,
334    ) -> MltResult<()> {
335        match self {
336            Self::None => *self = kind,
337            Self::Bool if matches!(kind, Self::Bool) => {}
338            Self::F32 if matches!(kind, Self::F32) => {}
339            Self::F64 if matches!(kind, Self::F64) => {}
340            _ => return mixed_prop_err(column_idx, property_name),
341        }
342        Ok(())
343    }
344}
345
346impl TileLayer {
347    /// Analyze a [`TileLayer`] and return reusable ID/property facts for the optimizer.
348    #[hotpath::measure]
349    pub(crate) fn analyze(&self, allow_shared_dict: bool) -> MltResult<LayerStats> {
350        let mut property_bits = Vec::with_capacity(self.property_names.len());
351        let mut properties = self.analyze_properties(&mut property_bits)?;
352        let id = self.analyze_ids(&property_bits);
353        if allow_shared_dict {
354            self.group_string_properties(&mut properties);
355        }
356        Ok(LayerStats { id, properties })
357    }
358
359    fn analyze_ids(&self, property_bits: &[(BitVec<u8>, usize)]) -> Option<PropertyStats> {
360        let mut min = u64::MAX;
361        let mut max = 0u64;
362        let mut bits = BitVec::<u8>::with_capacity(self.features.len());
363        for feature in &self.features {
364            if let Some(id) = feature.id {
365                min = min.min(id);
366                max = max.max(id);
367                bits.push(true);
368            } else {
369                bits.push(false);
370            }
371        }
372        let presence = Presence::from_bits(&bits, property_bits);
373        if presence == Presence::AllNull {
374            None
375        } else {
376            Some(PropertyStats {
377                presence,
378                stats: PropertyTypedStats::Unsigned { min, max },
379            })
380        }
381    }
382
383    fn analyze_properties(
384        &self,
385        property_bits: &mut Vec<(BitVec<u8>, usize)>,
386    ) -> MltResult<Vec<PropertyStats>> {
387        self.property_names
388            .iter()
389            .enumerate()
390            .map(|(col_idx, name)| -> MltResult<PropertyStats> {
391                let mut kind = None;
392                let mut stats = PropertyTypedStats::default();
393                let mut bits = BitVec::<u8>::with_capacity(self.features.len());
394                for feature in &self.features {
395                    let prop = feature.properties.get(col_idx);
396                    if let Some(prop_kind) = prop.map(PropKind::from) {
397                        match kind {
398                            Some(kind) if kind != prop_kind => {
399                                return mixed_prop_err(col_idx, name.as_str());
400                            }
401                            None => kind = Some(prop_kind),
402                            _ => {}
403                        }
404                    }
405                    if let Some(prop) = prop
406                        && stats.push(prop, col_idx, name)?
407                    {
408                        bits.push(true);
409                    } else {
410                        bits.push(false);
411                    }
412                }
413
414                let presence = Presence::from_bits(&bits, property_bits);
415                if presence == Presence::Mixed {
416                    property_bits.push((bits, col_idx));
417                }
418                Ok(PropertyStats { presence, stats })
419            })
420            .collect()
421    }
422}
423
424#[inline]
425fn mixed_prop_err<T>(column_idx: usize, property_name: &str) -> MltResult<T> {
426    Err(MltError::MixedPropertyTypes(
427        column_idx,
428        property_name.to_owned(),
429    ))
430}