Skip to main content

mlt_core/encoder/
optimizer.rs

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