Skip to main content

mlt_core/frames/v01/geometry/
optimizer.rs

1use std::collections::HashMap;
2
3use crate::codecs::morton::z_order_params;
4use crate::v01::encode::encode_geometry;
5use crate::v01::{
6    DataProfile, DictionaryType, EncodedGeometry, GeometryEncoder, GeometryValues, IntEncoder,
7    LengthType, OffsetType, StreamType, VertexBufferType,
8};
9use crate::{MltError, MltResult};
10
11/// If the ratio of unique vertices to total vertices is below this threshold,
12/// Morton dictionary encoding is preferred over Vec2 componentwise-delta.
13///
14/// A lower ratio means more coordinate repetition, which is precisely the
15/// scenario where the dictionary overhead pays off.
16const MORTON_UNIQUENESS_THRESHOLD: f64 = 0.5;
17
18/// A pre-computed set of per-stream [`IntEncoder`] candidates derived from a
19/// representative sample of tiles.
20///
21/// Building a profile once from sample tiles avoids re-running
22/// [`DataProfile::prune_candidates`] on every subsequent tile; the profile's
23/// per-stream candidate lists are used directly in the competition step instead.
24///
25/// Profiles from multiple samples are combined with [`GeometryProfile::merge`],
26/// which takes the union of each stream's candidate sets.
27#[derive(Debug, Clone, PartialEq)]
28pub struct GeometryProfile {
29    /// Per-stream encoder candidates to use during competition.
30    ///
31    /// An absent entry causes the caller to fall back to [`IntEncoder::auto_u32`].
32    stream_candidates: HashMap<StreamType, Vec<IntEncoder>>,
33}
34
35impl GeometryProfile {
36    #[doc(hidden)]
37    #[must_use]
38    pub fn new(stream_candidates: HashMap<StreamType, Vec<IntEncoder>>) -> Self {
39        Self { stream_candidates }
40    }
41
42    /// Build a profile from a sample of decoded geometry.
43    pub fn from_sample(decoded: &GeometryValues) -> MltResult<Self> {
44        let vertex_buffer_type = decoded
45            .vertices
46            .as_deref()
47            .map_or(VertexBufferType::Vec2, select_vertex_strategy);
48
49        let mut probe = GeometryEncoder::all(IntEncoder::varint());
50        probe.vertex_buffer_type(vertex_buffer_type);
51
52        let mut stream_candidates: HashMap<StreamType, Vec<IntEncoder>> = HashMap::new();
53        encode_geometry(
54            decoded,
55            &probe,
56            Some(&mut |st: StreamType, data: &[u32]| {
57                let candidates = DataProfile::prune_candidates::<i32>(data);
58                stream_candidates.insert(st, candidates);
59            }),
60        )?;
61
62        Ok(Self { stream_candidates })
63    }
64
65    /// Merge two profiles by taking the union of per-stream candidate sets.
66    ///
67    /// Encoders already present for a stream in `self` are not duplicated.
68    #[must_use]
69    pub fn merge(mut self, other: &Self) -> Self {
70        for (st, candidates) in &other.stream_candidates {
71            let entry = self.stream_candidates.entry(*st).or_default();
72            for &enc in candidates {
73                if !entry.contains(&enc) {
74                    entry.push(enc);
75                }
76            }
77        }
78        self
79    }
80}
81
82/// Analyze `decoded` and encode it with automatically selected per-stream encoders.
83///
84/// # Design
85///
86/// 1. **Probe** - call `encode_geometry` with an all-varint encoder and an
87///    `on_stream` callback that collects the raw `u32` payload for every stream.
88/// 2. **Select** - run [`IntEncoder::auto_u32`] on each payload to pick the best
89///    physical/logical combination per stream.
90fn optimize(decoded: &GeometryValues) -> MltResult<GeometryEncoder> {
91    let vertex_buffer_type = decoded
92        .vertices
93        .as_deref()
94        .map_or(VertexBufferType::Vec2, select_vertex_strategy);
95
96    // Pass 1: probe with an all-varint encoder and collect the raw u32
97    // payload for each stream via the on_stream callback.
98    let mut probe = GeometryEncoder::all(IntEncoder::varint());
99    probe.vertex_buffer_type(vertex_buffer_type);
100
101    let mut payloads: HashMap<StreamType, Vec<u32>> = HashMap::new();
102    encode_geometry(
103        decoded,
104        &probe,
105        Some(&mut |st: StreamType, data: &[u32]| {
106            payloads.insert(st, data.to_vec());
107        }),
108    )?;
109
110    let opt = |st: StreamType| -> IntEncoder {
111        payloads
112            .get(&st)
113            .map_or_else(IntEncoder::varint, |data| IntEncoder::auto_u32(data))
114    };
115
116    Ok(build_encoder(decoded, vertex_buffer_type, opt))
117}
118
119/// Apply a profile to `decoded`, re-deriving the vertex buffer strategy from
120/// the tile's actual data.
121///
122/// The same probe pass as [`optimize`] is performed, but competition is run
123/// over the profile's pre-computed per-stream candidate lists rather than
124/// re-running [`DataProfile::prune_candidates`] from scratch.
125fn apply_profile(
126    decoded: &GeometryValues,
127    profile: &GeometryProfile,
128) -> MltResult<GeometryEncoder> {
129    let vertex_buffer_type = decoded
130        .vertices
131        .as_deref()
132        .map_or(VertexBufferType::Vec2, select_vertex_strategy);
133
134    let mut probe = GeometryEncoder::all(IntEncoder::varint());
135    probe.vertex_buffer_type(vertex_buffer_type);
136
137    let mut payloads: HashMap<StreamType, Vec<u32>> = HashMap::new();
138    encode_geometry(
139        decoded,
140        &probe,
141        Some(&mut |st: StreamType, data: &[u32]| {
142            payloads.insert(st, data.to_vec());
143        }),
144    )?;
145
146    let opt = |st: StreamType| -> IntEncoder {
147        let data = payloads.get(&st);
148        let candidates = profile.stream_candidates.get(&st).map(Vec::as_slice);
149        match (data, candidates) {
150            (Some(data), Some(candidates)) if !candidates.is_empty() => {
151                DataProfile::compete_u32(candidates, data)
152            }
153            (Some(data), _) => IntEncoder::auto_u32(data),
154            _ => IntEncoder::varint(),
155        }
156    };
157
158    Ok(build_encoder(decoded, vertex_buffer_type, opt))
159}
160
161/// Assemble a [`GeometryEncoder`] from a stream-type-to-encoder mapping function.
162///
163/// Shared by [`optimize`] and [`apply_profile`]; the only difference between
164/// the two is how `opt` resolves the best [`IntEncoder`] for each stream.
165fn build_encoder(
166    decoded: &GeometryValues,
167    vertex_buffer_type: VertexBufferType,
168    mut opt: impl FnMut(StreamType) -> IntEncoder,
169) -> GeometryEncoder {
170    // Parts and Rings map to different GeometryEncoder fields depending on
171    // which topology branch fired.  Since only one branch can fire per call,
172    // each StreamType appears at most once in `payloads`.
173    let has_geom_offs = decoded.geometry_offsets.is_some();
174    let has_ring_offs = decoded.ring_offsets.is_some();
175
176    let parts_enc = opt(StreamType::Length(LengthType::Parts));
177    let rings_enc = opt(StreamType::Length(LengthType::Rings));
178
179    // The vertex data StreamType depends on the chosen layout strategy.
180    let vertex_st = match vertex_buffer_type {
181        VertexBufferType::Vec2 => StreamType::Data(DictionaryType::Vertex),
182        VertexBufferType::Morton => StreamType::Data(DictionaryType::Morton),
183    };
184
185    let mut encoder = GeometryEncoder::all(IntEncoder::varint());
186    encoder
187        .meta(opt(StreamType::Length(LengthType::VarBinary)))
188        .geometries(opt(StreamType::Length(LengthType::Geometries)))
189        .triangles(opt(StreamType::Length(LengthType::Triangles)))
190        .triangles_indexes(opt(StreamType::Offset(OffsetType::Index)))
191        .vertex(opt(vertex_st))
192        .vertex_offsets(opt(StreamType::Offset(OffsetType::Vertex)))
193        .vertex_buffer_type(vertex_buffer_type);
194
195    match (has_geom_offs, has_ring_offs) {
196        (true, true) => {
197            encoder.rings(parts_enc).rings2(rings_enc);
198        }
199        (true, false) => {
200            encoder.no_rings(parts_enc);
201        }
202        (false, true) => {
203            encoder.parts(parts_enc).parts_ring(rings_enc);
204        }
205        (false, false) => {
206            encoder.only_parts(parts_enc);
207        }
208    }
209    encoder
210}
211
212/// Choose between Vec2 componentwise-delta and Morton dictionary encoding.
213///
214/// Morton is only selected when:
215/// - The coordinate range fits within 16 bits per axis (required by the spec), and
216/// - The uniqueness ratio is below [`MORTON_UNIQUENESS_THRESHOLD`], meaning
217///   enough vertices are repeated that the dictionary overhead is worthwhile.
218fn select_vertex_strategy(vertices: &[i32]) -> VertexBufferType {
219    let total = vertices.len() / 2;
220    if total == 0 {
221        return VertexBufferType::Vec2;
222    }
223
224    // Morton requires coordinates in a bounded range.
225    if z_order_params(vertices).is_err() {
226        return VertexBufferType::Vec2;
227    }
228
229    // Count unique (x, y) pairs via a hash set.
230    let unique_count = vertices
231        .chunks_exact(2)
232        .map(|c| (c[0], c[1]))
233        .collect::<std::collections::HashSet<_>>()
234        .len();
235
236    #[expect(clippy::cast_precision_loss)]
237    let uniqueness_ratio = unique_count as f64 / total as f64;
238
239    if uniqueness_ratio < MORTON_UNIQUENESS_THRESHOLD {
240        VertexBufferType::Morton
241    } else {
242        VertexBufferType::Vec2
243    }
244}
245
246impl GeometryValues {
247    /// Encode this geometry using the given encoder, consuming `self`.
248    pub fn encode(self, encoder: GeometryEncoder) -> MltResult<EncodedGeometry> {
249        EncodedGeometry::encode(&self, encoder)
250    }
251
252    /// Encode this geometry using the profile to select the best encoder.
253    pub fn encode_with_profile(
254        &self,
255        profile: &GeometryProfile,
256    ) -> Result<(EncodedGeometry, GeometryEncoder), MltError> {
257        let enc = apply_profile(self, profile)?;
258        let encoded = EncodedGeometry::encode(self, enc)?;
259        Ok((encoded, enc))
260    }
261
262    /// Automatically select the best encoder and encode, consuming `self`.
263    pub fn encode_auto(self) -> Result<(EncodedGeometry, GeometryEncoder), MltError> {
264        let enc = optimize(&self)?;
265        let encoded = EncodedGeometry::encode(&self, enc)?;
266        Ok((encoded, enc))
267    }
268}