Skip to main content

mlt_core/decoder/stream/
model.rs

1use std::fmt::{Display, Formatter, Result as FmtResult};
2
3use derive_debug::Dbg;
4use num_enum::TryFromPrimitive;
5
6use crate::errors::AsMltError as _;
7#[cfg(feature = "unstable-v2")]
8use crate::tile::ZStep;
9use crate::utils::formatter::{bytes_dbg, compact_dbg};
10use crate::{MltError, MltResult};
11
12/// Logical encoding technique used for a column, as stored in the tile
13///
14/// Variants are already shifted into the primary logical field of the v1 encoding byte (bits 7-5),
15/// so that field is matched with a mask rather than shifted down first.
16/// The secondary field (bits 4-2) holds the same patterns three bits lower.
17#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
18#[repr(u8)]
19pub enum LogicalTechnique {
20    None = 0b0000_0000,
21    Delta = 0b0010_0000,
22    ComponentwiseDelta = 0b0100_0000,
23    Rle = 0b0110_0000,
24    Morton = 0b1000_0000,
25}
26
27/// The combinations of the two [`LogicalTechnique`] fields that are legal on the wire
28///
29/// Each variant is the whole logical part of the v1 encoding byte, i.e. the primary field
30/// (bits 7-5) or-ed with the secondary field (bits 4-2).
31/// Any other pairing of the two fields is rejected while parsing.
32#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
33#[repr(u8)]
34pub enum LogicalCombination {
35    None = 0b0000_0000,
36    Delta = 0b0010_0000,
37    DeltaRle = 0b0010_1100,
38    ComponentwiseDelta = 0b0100_0000,
39    Rle = 0b0110_0000,
40    MortonDelta = 0b1000_0100,
41}
42
43/// Which RLE stream layout the encoder should produce.
44///
45/// A data-less selector chosen up front by the wire format (see
46/// [`WireVersion::rle_layout`](crate::encoder::WireVersion)); the realized
47/// per-stream metadata is [`RleMeta`], whose variants mirror these.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum RleLayout {
50    /// Tag `0x01`: all run lengths first, then all values.
51    Split,
52    /// Tag `0x02`: `(run_length, value)` pairs. Requires the `unstable-v2` feature.
53    #[cfg(feature = "unstable-v2")]
54    Interleaved,
55}
56
57/// Metadata for RLE decoding, one variant per [`RleLayout`].
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum RleMeta {
60    /// Tag `0x01`: physically-decoded words are `[run_len × runs][value × runs]`.
61    /// `runs` is the split point; `num_rle_values` is the expanded element count.
62    Split { runs: u32, num_rle_values: u32 },
63    /// Tag `0x02`: physically-decoded words are `(run_len, value)` pairs. The run
64    /// count is derived from the data length, so only the expanded element count
65    /// (`num_rle_values`, from the stream's count context) is carried.
66    /// Requires the `unstable-v2` feature.
67    #[cfg(feature = "unstable-v2")]
68    Interleaved { num_rle_values: u32 },
69}
70
71impl RleMeta {
72    /// The total expanded element count, common to both layouts.
73    #[cfg(feature = "unstable-v2")]
74    #[must_use]
75    pub(crate) fn num_rle_values(self) -> u32 {
76        match self {
77            Self::Split { num_rle_values, .. } => num_rle_values,
78            #[cfg(feature = "unstable-v2")]
79            Self::Interleaved { num_rle_values } => num_rle_values,
80        }
81    }
82}
83
84/// Metadata for Morton decoding
85#[derive(Debug, Clone, Copy, PartialEq)]
86pub struct Morton {
87    /// Number of bits used
88    pub(crate) bits: u32,
89    /// Coordinate shift
90    pub(crate) shift: u32,
91}
92
93impl Morton {
94    pub fn new(bits: u32, shift: u32) -> MltResult<Self> {
95        if bits <= 16 {
96            Ok(Self { bits, shift })
97        } else {
98            Err(MltError::InvalidMortonBits(bits))
99        }
100    }
101}
102
103/// The decimal scaling a Framed, Exception-Free ALP column uses: `i = round(v * 10^e / 10^f)`.
104/// Chosen before any value is seen, so it carries no frame of reference.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct AlpScale {
107    /// Decimal exponent the values were scaled by.
108    pub(crate) e: u8,
109    /// Factor dividing out the trailing zeros `e` introduced, never exceeding `e`.
110    pub(crate) f: u8,
111}
112
113/// Framed, Exception-Free ALP parameters: `v = (base + offset) * 10^f / 10^e`.
114/// Written as a scale byte and a base varint, the stream itself holding the unsigned offsets.
115#[derive(Clone, Copy, PartialEq, Eq)]
116pub struct Alp {
117    pub(crate) scale: AlpScale,
118    /// Frame of reference the offsets are measured from, the smallest scaled integer in the column.
119    pub(crate) base: i64,
120}
121
122#[cfg(feature = "unstable-v2")]
123impl AlpScale {
124    /// Largest exponent the codes can carry, past which `v * 10^e` leaves the `i64` range.
125    /// The codes themselves are bounded tighter, to `2^53 - 1`, by the encoder.
126    pub(crate) const MAX_EXPONENT: u8 = 18;
127
128    /// Net power of ten the codes carry, which fixes their magnitude and so their stored size.
129    /// The order [`candidates`](crate::codecs::alp::candidates) walks, and so what lets the
130    /// encoder stop at the first scale that fits; only the tests holding that invariant name it.
131    #[cfg(test)]
132    pub(crate) fn net(self) -> u8 {
133        self.e - self.f
134    }
135
136    /// The header byte naming this scale, the pairs with `f <= e` numbered row by row.
137    pub(crate) fn to_byte(self) -> u8 {
138        Self::row_start(self.e) + self.f
139    }
140
141    /// Read a scale back from its header byte, rejecting the codes past the last pair.
142    pub(crate) fn from_byte(byte: u8) -> MltResult<Self> {
143        (0..=Self::MAX_EXPONENT)
144            .find_map(|e| {
145                let f = byte.checked_sub(Self::row_start(e))?;
146                (f <= e).then_some(Self { e, f })
147            })
148            .ok_or(MltError::InvalidAlpScale(byte))
149    }
150
151    /// First byte of the row for `e`, which is `e * (e + 1) / 2`.
152    fn row_start(e: u8) -> u8 {
153        (1..=e).sum()
154    }
155}
156
157/// Flattened, since the nesting is an encoder concern and these appear in stream labels.
158impl std::fmt::Debug for Alp {
159    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
160        write!(
161            f,
162            "Alp {{ e: {}, f: {}, base: {} }}",
163            self.scale.e, self.scale.f, self.base
164        )
165    }
166}
167
168#[cfg(feature = "unstable-v2")]
169impl Alp {
170    #[cfg(test)]
171    pub(crate) fn new(e: u8, f: u8, base: i64) -> MltResult<Self> {
172        if e <= AlpScale::MAX_EXPONENT && f <= e {
173            Ok(Self {
174                scale: AlpScale { e, f },
175                base,
176            })
177        } else {
178            Err(MltError::InvalidAlpParams(e, f))
179        }
180    }
181
182    /// Measure a scaled integer from the frame of reference, giving the offset the stream stores.
183    /// Wrapping, so a foreign stream whose codes span the whole `i64` range still subtracts
184    /// exactly; our own encoder keeps codes within `2^53 - 1`, so the spread fits `u64` easily.
185    #[expect(
186        clippy::cast_sign_loss,
187        reason = "the bit pattern is the point; `code_at` casts it back"
188    )]
189    pub(crate) fn offset_of(self, code: i64) -> u64 {
190        (code as u64).wrapping_sub(self.base as u64)
191    }
192
193    /// Put a stored offset back on the frame of reference, inverting [`Self::offset_of`].
194    #[expect(
195        clippy::cast_possible_wrap,
196        clippy::cast_sign_loss,
197        reason = "the bit pattern is the point; inverts `offset_of` exactly"
198    )]
199    pub(crate) fn code_at(self, offset: u64) -> i64 {
200        (self.base as u64).wrapping_add(offset) as i64
201    }
202}
203
204/// What kind of values a stream holds, which fixes the encodings it can name.
205/// Neither wire format stores it: v1 reads it from the column type, v2 from the stream's context.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ValueKind {
208    Int,
209    Bool,
210    Float,
211    Vertex,
212}
213
214/// Logical encoding of a stream of integer values.
215/// Covers the id columns, the integer property columns, and the geometry length and offset streams.
216#[derive(Debug, Clone, Copy, PartialEq)]
217pub enum IntLogical {
218    None,
219    Delta,
220    Rle(RleMeta),
221    DeltaRle(RleMeta),
222    /// Deltas of the deltas, both running from zero.
223    Delta2,
224}
225
226/// Logical encoding of a bool column's data stream or a presence bitfield.
227#[derive(Debug, Clone, Copy, PartialEq)]
228pub enum BoolLogical {
229    /// A raw packed bitmap, one bit per value.
230    None,
231    /// Alternating run lengths, which only the tag `0x02` codec reads or writes.
232    Runs,
233    /// A bitmap of the bitmap's non-zero bytes and then those bytes, which only the tag `0x02` codec reads or writes.
234    Sparse,
235    /// A byte-RLE compressed bitmap.
236    /// Its run parameters come from the stream's context rather than from its header.
237    ByteRle(RleMeta),
238}
239
240/// Logical encoding of a float column's data stream.
241#[derive(Debug, Clone, Copy, PartialEq)]
242pub enum FloatLogical {
243    /// Fixed-width little-endian values, one per element.
244    None,
245    /// A decimal split across two integer streams, which v1 can name but no decoder here implements.
246    /// One code per element, with the distinct values following as a second stream.
247    /// Only the tag `0x02` codec reads or writes it.
248    Dict,
249    /// Integers scaled by these parameters.
250    /// Only the tag `0x02` codec reads or writes it.
251    Alp(Alp),
252}
253
254/// Logical encoding of a geometry vertex stream, whose values are interleaved coordinate pairs.
255#[derive(Debug, Clone, Copy, PartialEq)]
256pub enum VertexLogical {
257    None,
258    Delta,
259    ComponentwiseDelta,
260    /// Componentwise deltas of the componentwise deltas.
261    ComponentwiseDelta2,
262    MortonDelta(Morton),
263    /// rANS over componentwise deltas, coded per run of the layer's topology.
264    Rans,
265    /// Interleaved `(x, y, z)` triples, whose z lie on the grid of the step.
266    #[cfg(feature = "unstable-v2")]
267    Xyz(ZStep, XyzLogical),
268}
269
270impl VertexLogical {
271    /// The 32-bit words one vertex spans, where a Morton code is one word.
272    #[must_use]
273    pub fn words_per_vertex(self) -> u32 {
274        match self {
275            Self::None
276            | Self::Delta
277            | Self::ComponentwiseDelta
278            | Self::ComponentwiseDelta2
279            | Self::Rans => 2,
280            Self::MortonDelta(_) => 1,
281            #[cfg(feature = "unstable-v2")]
282            Self::Xyz(..) => 3,
283        }
284    }
285}
286
287/// Logical encoding of an `(x, y, z)` vertex stream.
288///
289/// A Morton code spans only `x` and `y`, so there is no Morton member.
290#[cfg(feature = "unstable-v2")]
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum XyzLogical {
293    None,
294    Delta,
295    ComponentwiseDelta,
296}
297
298/// How should the stream be interpreted at the logical level (second pass of decoding)
299///
300/// Split per [`ValueKind`] so a stream can name only the encodings its values can have.
301#[derive(Debug, Clone, Copy, PartialEq)]
302pub enum LogicalEncoding {
303    Int(IntLogical),
304    Bool(BoolLogical),
305    Float(FloatLogical),
306    Vertex(VertexLogical),
307}
308
309impl LogicalEncoding {
310    /// The 32-bit words one counted value spans, which only a vertex stream makes more than one.
311    #[must_use]
312    pub(crate) fn words_per_value(self) -> u32 {
313        match self {
314            Self::Vertex(logical) => logical.words_per_vertex(),
315            Self::Int(_) | Self::Bool(_) | Self::Float(_) => 1,
316        }
317    }
318
319    /// The kind of values this encoding belongs to.
320    #[must_use]
321    pub fn kind(self) -> ValueKind {
322        match self {
323            Self::Int(_) => ValueKind::Int,
324            Self::Bool(_) => ValueKind::Bool,
325            Self::Float(_) => ValueKind::Float,
326            Self::Vertex(_) => ValueKind::Vertex,
327        }
328    }
329
330    /// The identity encoding for `kind`, i.e. values stored as they are.
331    #[must_use]
332    pub(crate) fn none(kind: ValueKind) -> Self {
333        match kind {
334            ValueKind::Int => Self::Int(IntLogical::None),
335            ValueKind::Bool => Self::Bool(BoolLogical::None),
336            ValueKind::Float => Self::Float(FloatLogical::None),
337            ValueKind::Vertex => Self::Vertex(VertexLogical::None),
338        }
339    }
340
341    /// Whether the stream's own logical pass is a no-op, so the physical words are already the output.
342    /// True for a float dictionary's codes, which the column turns back into floats.
343    /// Not true for Framed, Exception-Free ALP, whose offsets still need the frame of reference added back.
344    #[must_use]
345    pub(crate) fn is_identity(self) -> bool {
346        matches!(
347            self,
348            Self::Int(IntLogical::None)
349                | Self::Bool(BoolLogical::None)
350                | Self::Float(FloatLogical::None | FloatLogical::Dict)
351                | Self::Vertex(VertexLogical::None)
352        )
353    }
354}
355
356/// Carries the stream metadata needed to perform the logical decode pass.
357///
358/// Construct with [`LogicalValue::new`] after the physical decode pass fills a
359/// `&[u32]` or `&[u64]` buffer, then call the appropriate `decode_*` method,
360/// passing that slice as `data`.
361#[derive(Debug, PartialEq)]
362pub struct LogicalValue {
363    pub(crate) meta: StreamMeta,
364}
365
366// Physical encoding types
367
368/// Dictionary type used for a column, as stored in the tile
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
370#[repr(u8)]
371pub enum DictionaryType {
372    None = 0b0000_0000,
373    Single = 0b0000_0001,
374    Shared = 0b0000_0010,
375    Vertex = 0b0000_0011,
376    Morton = 0b0000_0100,
377    Fsst = 0b0000_0101,
378}
379
380/// Offset type used for a column, as stored in the tile
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
382#[repr(u8)]
383pub enum OffsetType {
384    Vertex = 0b0000_0000,
385    Index = 0b0000_0001,
386    String = 0b0000_0010,
387    Key = 0b0000_0011,
388}
389
390/// Length type used for a column, as stored in the tile
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
392#[repr(u8)]
393pub enum LengthType {
394    VarBinary = 0b0000_0000,
395    Geometries = 0b0000_0001,
396    Parts = 0b0000_0010,
397    Rings = 0b0000_0011,
398    Triangles = 0b0000_0100,
399    Symbol = 0b0000_0101,
400    Dictionary = 0b0000_0110,
401    /// A nested list or map node's lengths, one per present value of the node.
402    #[cfg(feature = "unstable-v2")]
403    Nested = 0b0000_0111,
404}
405
406/// How should the stream be interpreted at the physical level (first pass of decoding)
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
408pub enum StreamType {
409    Present,
410    Data(DictionaryType),
411    Offset(OffsetType),
412    Length(LengthType),
413}
414
415/// Physical encoding used for a column, as stored in the tile
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
417pub enum PhysicalEncoding {
418    None,
419    /// Preferred, tends to produce the best compression ratio and decoding performance.
420    /// But currently limited to 32-bit integer.
421    FastPFor(FastPForKind),
422    /// Can produce better results in combination with a heavyweight compression scheme like `Gzip`.
423    /// Simple compression scheme where the encoding is easier to implement compared to `FastPfor`.
424    VarInt,
425    /// Every value in the same number of bits, which a leading payload byte names.
426    /// Beats `VarInt` on the short, small-alphabet streams a dictionary's codes form,
427    /// where `FastPFor`'s 128-value blocks are too coarse to pay for themselves.
428    #[cfg(feature = "unstable-v2")]
429    BitPacked,
430}
431
432/// The `FastPFor` block size and word order a wire version codes its integer streams with.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
434pub enum FastPForKind {
435    /// 256-value blocks over big-endian words
436    Block256Be,
437    /// 128-value blocks over little-endian words
438    #[cfg(feature = "unstable-v2")]
439    Block128Le,
440}
441
442// RawStream types
443
444#[derive(Debug, Clone, Copy, PartialEq)]
445pub struct IntEncoding {
446    pub logical: LogicalEncoding,
447    pub physical: PhysicalEncoding,
448}
449
450impl IntEncoding {
451    #[must_use]
452    pub(crate) const fn new(logical: LogicalEncoding, physical: PhysicalEncoding) -> Self {
453        Self { logical, physical }
454    }
455
456    #[must_use]
457    pub(crate) fn none(kind: ValueKind) -> Self {
458        Self::new(LogicalEncoding::none(kind), PhysicalEncoding::None)
459    }
460}
461
462/// Metadata about an encoded stream
463#[derive(Clone, Copy, Dbg, PartialEq)]
464pub struct StreamMeta {
465    #[dbg(formatter = "compact_dbg")]
466    pub stream_type: StreamType,
467    #[dbg(formatter = "compact_dbg")]
468    pub encoding: IntEncoding,
469    pub(crate) num_values: u32,
470}
471
472impl StreamMeta {
473    #[inline]
474    pub(crate) fn new(stream_type: StreamType, encoding: IntEncoding, num_values: u32) -> Self {
475        Self {
476            stream_type,
477            encoding,
478            num_values,
479        }
480    }
481
482    /// A meta for a stream of `num_words` physical words, which a vertex stream counts in vertices.
483    #[inline]
484    pub(crate) fn new2(
485        stream_type: StreamType,
486        logical: LogicalEncoding,
487        physical: PhysicalEncoding,
488        num_words: usize,
489    ) -> MltResult<Self> {
490        let enc = IntEncoding::new(logical, physical);
491        let num_words = u32::try_from(num_words)?;
492        let per_value = logical.words_per_value();
493        if !num_words.is_multiple_of(per_value) {
494            return Err(MltError::PartialVertex(num_words, per_value));
495        }
496        Ok(Self::new(stream_type, enc, num_words / per_value))
497    }
498
499    /// The physical words the stream's values span.
500    #[inline]
501    pub(crate) fn num_words(&self) -> MltResult<u32> {
502        self.num_values
503            .checked_mul(self.encoding.logical.words_per_value())
504            .or_overflow()
505    }
506
507    #[inline]
508    pub(crate) fn new_none(
509        stream_type: StreamType,
510        kind: ValueKind,
511        num_values: usize,
512    ) -> MltResult<Self> {
513        let enc = IntEncoding::none(kind);
514        Ok(Self::new(stream_type, enc, u32::try_from(num_values)?))
515    }
516}
517
518/// Representation of an encoded stream
519#[derive(Clone, Dbg, PartialEq)]
520pub struct RawStream<'a> {
521    pub meta: StreamMeta,
522    #[dbg(formatter = "bytes_dbg")]
523    pub(crate) data: &'a [u8],
524}
525
526impl<'a> RawStream<'a> {
527    #[must_use]
528    pub(crate) fn new(meta: StreamMeta, data: &'a [u8]) -> Self {
529        Self { meta, data }
530    }
531}
532
533// Display impls the annotated dump's JSON carries instead of serde derives on this
534// vocabulary, so the wire enums do not become frozen public JSON API.
535
536impl Display for StreamType {
537    /// `present`, `data`, `data[fsst]`, `offset[string]`, `length[rings]`.
538    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
539        match self {
540            Self::Present => f.write_str("present"),
541            Self::Data(dict) => {
542                let name = match dict {
543                    DictionaryType::None => return f.write_str("data"),
544                    DictionaryType::Single => "single",
545                    DictionaryType::Shared => "shared",
546                    DictionaryType::Vertex => "vertex",
547                    DictionaryType::Morton => "morton",
548                    DictionaryType::Fsst => "fsst",
549                };
550                write!(f, "data[{name}]")
551            }
552            Self::Offset(offset) => {
553                let name = match offset {
554                    OffsetType::Vertex => "vertex",
555                    OffsetType::Index => "index",
556                    OffsetType::String => "string",
557                    OffsetType::Key => "key",
558                };
559                write!(f, "offset[{name}]")
560            }
561            Self::Length(length) => {
562                let name = match length {
563                    LengthType::VarBinary => "var-binary",
564                    LengthType::Geometries => "geometries",
565                    LengthType::Parts => "parts",
566                    LengthType::Rings => "rings",
567                    LengthType::Triangles => "triangles",
568                    LengthType::Symbol => "symbol",
569                    LengthType::Dictionary => "dictionary",
570                    #[cfg(feature = "unstable-v2")]
571                    LengthType::Nested => "nested",
572                };
573                write!(f, "length[{name}]")
574            }
575        }
576    }
577}
578
579impl Display for LogicalEncoding {
580    /// The value kind, then its encoding: `int/rle`, `float/alp`, `vertex/morton-delta`.
581    ///
582    /// Run parameters are left out, they belong to the region's own annotation.
583    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
584        let (kind, enc) = match self {
585            Self::Int(int) => (
586                "int",
587                match int {
588                    IntLogical::None => "none",
589                    IntLogical::Delta => "delta",
590                    IntLogical::Rle(_) => "rle",
591                    IntLogical::DeltaRle(_) => "delta-rle",
592                    IntLogical::Delta2 => "delta2",
593                },
594            ),
595            Self::Bool(b) => (
596                "bool",
597                match b {
598                    BoolLogical::None => "none",
599                    BoolLogical::Runs => "runs",
600                    BoolLogical::Sparse => "sparse",
601                    BoolLogical::ByteRle(_) => "byte-rle",
602                },
603            ),
604            Self::Float(float) => (
605                "float",
606                match float {
607                    FloatLogical::None => "none",
608                    FloatLogical::Dict => "dict",
609                    FloatLogical::Alp(_) => "alp",
610                },
611            ),
612            Self::Vertex(vertex) => (
613                "vertex",
614                match vertex {
615                    VertexLogical::None => "none",
616                    VertexLogical::Delta => "delta",
617                    VertexLogical::ComponentwiseDelta => "componentwise-delta",
618                    VertexLogical::ComponentwiseDelta2 => "componentwise-delta2",
619                    VertexLogical::MortonDelta(_) => "morton-delta",
620                    VertexLogical::Rans => "rans",
621                    #[cfg(feature = "unstable-v2")]
622                    VertexLogical::Xyz(_, XyzLogical::None) => "xyz-none",
623                    #[cfg(feature = "unstable-v2")]
624                    VertexLogical::Xyz(_, XyzLogical::Delta) => "xyz-delta",
625                    #[cfg(feature = "unstable-v2")]
626                    VertexLogical::Xyz(_, XyzLogical::ComponentwiseDelta) => {
627                        "xyz-componentwise-delta"
628                    }
629                },
630            ),
631        };
632        write!(f, "{kind}/{enc}")
633    }
634}
635
636impl Display for PhysicalEncoding {
637    /// `none`, `varint`, `fastpfor[256be]`, `bit-packed`.
638    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
639        match self {
640            Self::None => f.write_str("none"),
641            Self::VarInt => f.write_str("varint"),
642            Self::FastPFor(kind) => {
643                let name = match kind {
644                    FastPForKind::Block256Be => "256be",
645                    #[cfg(feature = "unstable-v2")]
646                    FastPForKind::Block128Le => "128le",
647                };
648                write!(f, "fastpfor[{name}]")
649            }
650            #[cfg(feature = "unstable-v2")]
651            Self::BitPacked => f.write_str("bit-packed"),
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use rstest::rstest;
659    use usize_cast::IntoUsize as _;
660
661    use super::*;
662
663    fn morton() -> Morton {
664        Morton { bits: 4, shift: 0 }
665    }
666
667    fn alp() -> Alp {
668        Alp {
669            scale: AlpScale { e: 3, f: 1 },
670            base: -5,
671        }
672    }
673
674    fn rle() -> RleMeta {
675        RleMeta::Split {
676            runs: 2,
677            num_rle_values: 5,
678        }
679    }
680
681    #[rstest]
682    #[case::present(StreamType::Present, "present")]
683    #[case::data_none(StreamType::Data(DictionaryType::None), "data")]
684    #[case::data_single(StreamType::Data(DictionaryType::Single), "data[single]")]
685    #[case::data_shared(StreamType::Data(DictionaryType::Shared), "data[shared]")]
686    #[case::data_vertex(StreamType::Data(DictionaryType::Vertex), "data[vertex]")]
687    #[case::data_morton(StreamType::Data(DictionaryType::Morton), "data[morton]")]
688    #[case::data_fsst(StreamType::Data(DictionaryType::Fsst), "data[fsst]")]
689    #[case::offset_vertex(StreamType::Offset(OffsetType::Vertex), "offset[vertex]")]
690    #[case::offset_index(StreamType::Offset(OffsetType::Index), "offset[index]")]
691    #[case::offset_string(StreamType::Offset(OffsetType::String), "offset[string]")]
692    #[case::offset_key(StreamType::Offset(OffsetType::Key), "offset[key]")]
693    #[case::length_var_binary(StreamType::Length(LengthType::VarBinary), "length[var-binary]")]
694    #[case::length_geometries(StreamType::Length(LengthType::Geometries), "length[geometries]")]
695    #[case::length_parts(StreamType::Length(LengthType::Parts), "length[parts]")]
696    #[case::length_rings(StreamType::Length(LengthType::Rings), "length[rings]")]
697    #[case::length_triangles(StreamType::Length(LengthType::Triangles), "length[triangles]")]
698    #[case::length_symbol(StreamType::Length(LengthType::Symbol), "length[symbol]")]
699    #[case::length_dictionary(StreamType::Length(LengthType::Dictionary), "length[dictionary]")]
700    fn every_stream_type_renders_its_wire_label(
701        #[case] stream_type: StreamType,
702        #[case] expected: &str,
703    ) {
704        assert_eq!(stream_type.to_string(), expected);
705    }
706
707    #[cfg(feature = "unstable-v2")]
708    #[test]
709    fn a_nested_length_stream_renders_its_wire_label() {
710        assert_eq!(
711            StreamType::Length(LengthType::Nested).to_string(),
712            "length[nested]"
713        );
714    }
715
716    #[rstest]
717    #[case::int_none(LogicalEncoding::Int(IntLogical::None), "int/none")]
718    #[case::int_delta(LogicalEncoding::Int(IntLogical::Delta), "int/delta")]
719    #[case::int_rle(LogicalEncoding::Int(IntLogical::Rle(rle())), "int/rle")]
720    #[case::int_delta_rle(LogicalEncoding::Int(IntLogical::DeltaRle(rle())), "int/delta-rle")]
721    #[case::bool_none(LogicalEncoding::Bool(BoolLogical::None), "bool/none")]
722    #[case::bool_byte_rle(LogicalEncoding::Bool(BoolLogical::ByteRle(rle())), "bool/byte-rle")]
723    #[case::float_none(LogicalEncoding::Float(FloatLogical::None), "float/none")]
724    #[case::float_dict(LogicalEncoding::Float(FloatLogical::Dict), "float/dict")]
725    #[case::float_alp(LogicalEncoding::Float(FloatLogical::Alp(alp())), "float/alp")]
726    #[case::vertex_none(LogicalEncoding::Vertex(VertexLogical::None), "vertex/none")]
727    #[case::vertex_delta(LogicalEncoding::Vertex(VertexLogical::Delta), "vertex/delta")]
728    #[case::vertex_componentwise_delta(
729        LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta),
730        "vertex/componentwise-delta"
731    )]
732    #[case::vertex_morton_delta(
733        LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())),
734        "vertex/morton-delta"
735    )]
736    fn every_logical_encoding_renders_kind_then_encoding(
737        #[case] encoding: LogicalEncoding,
738        #[case] expected: &str,
739    ) {
740        assert_eq!(encoding.to_string(), expected);
741    }
742
743    #[cfg(feature = "unstable-v2")]
744    #[rstest]
745    #[case::none(XyzLogical::None, "vertex/xyz-none")]
746    #[case::delta(XyzLogical::Delta, "vertex/xyz-delta")]
747    #[case::componentwise_delta(XyzLogical::ComponentwiseDelta, "vertex/xyz-componentwise-delta")]
748    fn every_xyz_encoding_renders_kind_then_encoding(
749        #[case] xyz: XyzLogical,
750        #[case] expected: &str,
751    ) {
752        let step = ZStep::new(0).unwrap();
753        assert_eq!(
754            LogicalEncoding::Vertex(VertexLogical::Xyz(step, xyz)).to_string(),
755            expected
756        );
757    }
758
759    #[rstest]
760    #[case::none(PhysicalEncoding::None, "none")]
761    #[case::varint(PhysicalEncoding::VarInt, "varint")]
762    #[case::fastpfor_256be(
763        PhysicalEncoding::FastPFor(FastPForKind::Block256Be),
764        "fastpfor[256be]"
765    )]
766    fn every_physical_encoding_renders_its_wire_label(
767        #[case] encoding: PhysicalEncoding,
768        #[case] expected: &str,
769    ) {
770        assert_eq!(encoding.to_string(), expected);
771    }
772
773    #[cfg(feature = "unstable-v2")]
774    #[rstest]
775    #[case::fastpfor_128le(
776        PhysicalEncoding::FastPFor(FastPForKind::Block128Le),
777        "fastpfor[128le]"
778    )]
779    #[case::bit_packed(PhysicalEncoding::BitPacked, "bit-packed")]
780    fn every_v2_physical_encoding_renders_its_wire_label(
781        #[case] encoding: PhysicalEncoding,
782        #[case] expected: &str,
783    ) {
784        assert_eq!(encoding.to_string(), expected);
785    }
786
787    #[rstest]
788    #[case::ints(LogicalEncoding::Int(IntLogical::Delta), 6, 6)]
789    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta), 6, 3)]
790    #[case::morton_codes(LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())), 6, 6)]
791    #[cfg_attr(feature = "unstable-v2", case::triples(
792        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
793        6,
794        2
795    ))]
796    fn a_stream_counts_its_words_as_values(
797        #[case] logical: LogicalEncoding,
798        #[case] words: usize,
799        #[case] values: u32,
800    ) {
801        let meta = StreamMeta::new2(
802            StreamType::Present,
803            logical,
804            PhysicalEncoding::VarInt,
805            words,
806        )
807        .unwrap();
808        assert_eq!(meta.num_values, values);
809        assert_eq!(meta.num_words().unwrap().into_usize(), words);
810    }
811
812    #[rstest]
813    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::Delta), 5, 2)]
814    #[cfg_attr(feature = "unstable-v2", case::triples(
815        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
816        4,
817        3
818    ))]
819    fn a_stream_of_partial_vertices_is_rejected(
820        #[case] logical: LogicalEncoding,
821        #[case] words: u32,
822        #[case] per_vertex: u32,
823    ) {
824        let err = StreamMeta::new2(
825            StreamType::Present,
826            logical,
827            PhysicalEncoding::VarInt,
828            words.into_usize(),
829        )
830        .unwrap_err();
831        assert!(
832            matches!(err, MltError::PartialVertex(w, p) if w == words && p == per_vertex),
833            "{err:?}"
834        );
835    }
836
837    #[rstest]
838    #[case::int(LogicalEncoding::Int(IntLogical::None), ValueKind::Int)]
839    #[case::bool(LogicalEncoding::Bool(BoolLogical::None), ValueKind::Bool)]
840    #[case::float(LogicalEncoding::Float(FloatLogical::None), ValueKind::Float)]
841    #[case::vertex(LogicalEncoding::Vertex(VertexLogical::None), ValueKind::Vertex)]
842    fn the_identity_encoding_of_a_kind_reports_that_kind_back(
843        #[case] expected: LogicalEncoding,
844        #[case] kind: ValueKind,
845    ) {
846        let none = LogicalEncoding::none(kind);
847        assert_eq!(none, expected);
848        assert_eq!(none.kind(), kind);
849        assert!(none.is_identity());
850    }
851}