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    /// A byte-RLE compressed bitmap.
232    /// Its run parameters come from the stream's context rather than from its header.
233    ByteRle(RleMeta),
234}
235
236/// Logical encoding of a float column's data stream.
237#[derive(Debug, Clone, Copy, PartialEq)]
238pub enum FloatLogical {
239    /// Fixed-width little-endian values, one per element.
240    None,
241    /// A decimal split across two integer streams, which v1 can name but no decoder here implements.
242    /// One code per element, with the distinct values following as a second stream.
243    /// Only the tag `0x02` codec reads or writes it.
244    Dict,
245    /// Integers scaled by these parameters.
246    /// Only the tag `0x02` codec reads or writes it.
247    Alp(Alp),
248}
249
250/// Logical encoding of a geometry vertex stream, whose values are interleaved coordinate pairs.
251#[derive(Debug, Clone, Copy, PartialEq)]
252pub enum VertexLogical {
253    None,
254    Delta,
255    ComponentwiseDelta,
256    /// Componentwise deltas of the componentwise deltas.
257    ComponentwiseDelta2,
258    MortonDelta(Morton),
259    /// rANS over componentwise deltas, coded per run of the layer's topology.
260    Rans,
261    /// Interleaved `(x, y, z)` triples, whose z lie on the grid of the step.
262    #[cfg(feature = "unstable-v2")]
263    Xyz(ZStep, XyzLogical),
264}
265
266impl VertexLogical {
267    /// The 32-bit words one vertex spans, where a Morton code is one word.
268    #[must_use]
269    pub fn words_per_vertex(self) -> u32 {
270        match self {
271            Self::None
272            | Self::Delta
273            | Self::ComponentwiseDelta
274            | Self::ComponentwiseDelta2
275            | Self::Rans => 2,
276            Self::MortonDelta(_) => 1,
277            #[cfg(feature = "unstable-v2")]
278            Self::Xyz(..) => 3,
279        }
280    }
281}
282
283/// Logical encoding of an `(x, y, z)` vertex stream.
284///
285/// A Morton code spans only `x` and `y`, so there is no Morton member.
286#[cfg(feature = "unstable-v2")]
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum XyzLogical {
289    None,
290    Delta,
291    ComponentwiseDelta,
292}
293
294/// How should the stream be interpreted at the logical level (second pass of decoding)
295///
296/// Split per [`ValueKind`] so a stream can name only the encodings its values can have.
297#[derive(Debug, Clone, Copy, PartialEq)]
298pub enum LogicalEncoding {
299    Int(IntLogical),
300    Bool(BoolLogical),
301    Float(FloatLogical),
302    Vertex(VertexLogical),
303}
304
305impl LogicalEncoding {
306    /// The 32-bit words one counted value spans, which only a vertex stream makes more than one.
307    #[must_use]
308    pub(crate) fn words_per_value(self) -> u32 {
309        match self {
310            Self::Vertex(logical) => logical.words_per_vertex(),
311            Self::Int(_) | Self::Bool(_) | Self::Float(_) => 1,
312        }
313    }
314
315    /// The kind of values this encoding belongs to.
316    #[must_use]
317    pub fn kind(self) -> ValueKind {
318        match self {
319            Self::Int(_) => ValueKind::Int,
320            Self::Bool(_) => ValueKind::Bool,
321            Self::Float(_) => ValueKind::Float,
322            Self::Vertex(_) => ValueKind::Vertex,
323        }
324    }
325
326    /// The identity encoding for `kind`, i.e. values stored as they are.
327    #[must_use]
328    pub(crate) fn none(kind: ValueKind) -> Self {
329        match kind {
330            ValueKind::Int => Self::Int(IntLogical::None),
331            ValueKind::Bool => Self::Bool(BoolLogical::None),
332            ValueKind::Float => Self::Float(FloatLogical::None),
333            ValueKind::Vertex => Self::Vertex(VertexLogical::None),
334        }
335    }
336
337    /// Whether the stream's own logical pass is a no-op, so the physical words are already the output.
338    /// True for a float dictionary's codes, which the column turns back into floats.
339    /// Not true for Framed, Exception-Free ALP, whose offsets still need the frame of reference added back.
340    #[must_use]
341    pub(crate) fn is_identity(self) -> bool {
342        matches!(
343            self,
344            Self::Int(IntLogical::None)
345                | Self::Bool(BoolLogical::None)
346                | Self::Float(FloatLogical::None | FloatLogical::Dict)
347                | Self::Vertex(VertexLogical::None)
348        )
349    }
350}
351
352/// Carries the stream metadata needed to perform the logical decode pass.
353///
354/// Construct with [`LogicalValue::new`] after the physical decode pass fills a
355/// `&[u32]` or `&[u64]` buffer, then call the appropriate `decode_*` method,
356/// passing that slice as `data`.
357#[derive(Debug, PartialEq)]
358pub struct LogicalValue {
359    pub(crate) meta: StreamMeta,
360}
361
362// Physical encoding types
363
364/// Dictionary type used for a column, as stored in the tile
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
366#[repr(u8)]
367pub enum DictionaryType {
368    None = 0b0000_0000,
369    Single = 0b0000_0001,
370    Shared = 0b0000_0010,
371    Vertex = 0b0000_0011,
372    Morton = 0b0000_0100,
373    Fsst = 0b0000_0101,
374}
375
376/// Offset type used for a column, as stored in the tile
377#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
378#[repr(u8)]
379pub enum OffsetType {
380    Vertex = 0b0000_0000,
381    Index = 0b0000_0001,
382    String = 0b0000_0010,
383    Key = 0b0000_0011,
384}
385
386/// Length type used for a column, as stored in the tile
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, TryFromPrimitive)]
388#[repr(u8)]
389pub enum LengthType {
390    VarBinary = 0b0000_0000,
391    Geometries = 0b0000_0001,
392    Parts = 0b0000_0010,
393    Rings = 0b0000_0011,
394    Triangles = 0b0000_0100,
395    Symbol = 0b0000_0101,
396    Dictionary = 0b0000_0110,
397    /// A nested list or map node's lengths, one per present value of the node.
398    #[cfg(feature = "unstable-v2")]
399    Nested = 0b0000_0111,
400}
401
402/// How should the stream be interpreted at the physical level (first pass of decoding)
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
404pub enum StreamType {
405    Present,
406    Data(DictionaryType),
407    Offset(OffsetType),
408    Length(LengthType),
409}
410
411/// Physical encoding used for a column, as stored in the tile
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
413pub enum PhysicalEncoding {
414    None,
415    /// Preferred, tends to produce the best compression ratio and decoding performance.
416    /// But currently limited to 32-bit integer.
417    FastPFor(FastPForKind),
418    /// Can produce better results in combination with a heavyweight compression scheme like `Gzip`.
419    /// Simple compression scheme where the encoding is easier to implement compared to `FastPfor`.
420    VarInt,
421    /// Every value in the same number of bits, which a leading payload byte names.
422    /// Beats `VarInt` on the short, small-alphabet streams a dictionary's codes form,
423    /// where `FastPFor`'s 128-value blocks are too coarse to pay for themselves.
424    #[cfg(feature = "unstable-v2")]
425    BitPacked,
426}
427
428/// The `FastPFor` block size and word order a wire version codes its integer streams with.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
430pub enum FastPForKind {
431    /// 256-value blocks over big-endian words
432    Block256Be,
433    /// 128-value blocks over little-endian words
434    #[cfg(feature = "unstable-v2")]
435    Block128Le,
436}
437
438// RawStream types
439
440#[derive(Debug, Clone, Copy, PartialEq)]
441pub struct IntEncoding {
442    pub logical: LogicalEncoding,
443    pub physical: PhysicalEncoding,
444}
445
446impl IntEncoding {
447    #[must_use]
448    pub(crate) const fn new(logical: LogicalEncoding, physical: PhysicalEncoding) -> Self {
449        Self { logical, physical }
450    }
451
452    #[must_use]
453    pub(crate) fn none(kind: ValueKind) -> Self {
454        Self::new(LogicalEncoding::none(kind), PhysicalEncoding::None)
455    }
456}
457
458/// Metadata about an encoded stream
459#[derive(Clone, Copy, Dbg, PartialEq)]
460pub struct StreamMeta {
461    #[dbg(formatter = "compact_dbg")]
462    pub stream_type: StreamType,
463    #[dbg(formatter = "compact_dbg")]
464    pub encoding: IntEncoding,
465    pub(crate) num_values: u32,
466}
467
468impl StreamMeta {
469    #[inline]
470    pub(crate) fn new(stream_type: StreamType, encoding: IntEncoding, num_values: u32) -> Self {
471        Self {
472            stream_type,
473            encoding,
474            num_values,
475        }
476    }
477
478    /// A meta for a stream of `num_words` physical words, which a vertex stream counts in vertices.
479    #[inline]
480    pub(crate) fn new2(
481        stream_type: StreamType,
482        logical: LogicalEncoding,
483        physical: PhysicalEncoding,
484        num_words: usize,
485    ) -> MltResult<Self> {
486        let enc = IntEncoding::new(logical, physical);
487        let num_words = u32::try_from(num_words)?;
488        let per_value = logical.words_per_value();
489        if !num_words.is_multiple_of(per_value) {
490            return Err(MltError::PartialVertex(num_words, per_value));
491        }
492        Ok(Self::new(stream_type, enc, num_words / per_value))
493    }
494
495    /// The physical words the stream's values span.
496    #[inline]
497    pub(crate) fn num_words(&self) -> MltResult<u32> {
498        self.num_values
499            .checked_mul(self.encoding.logical.words_per_value())
500            .or_overflow()
501    }
502
503    #[inline]
504    pub(crate) fn new_none(
505        stream_type: StreamType,
506        kind: ValueKind,
507        num_values: usize,
508    ) -> MltResult<Self> {
509        let enc = IntEncoding::none(kind);
510        Ok(Self::new(stream_type, enc, u32::try_from(num_values)?))
511    }
512}
513
514/// Representation of an encoded stream
515#[derive(Clone, Dbg, PartialEq)]
516pub struct RawStream<'a> {
517    pub meta: StreamMeta,
518    #[dbg(formatter = "bytes_dbg")]
519    pub(crate) data: &'a [u8],
520}
521
522impl<'a> RawStream<'a> {
523    #[must_use]
524    pub(crate) fn new(meta: StreamMeta, data: &'a [u8]) -> Self {
525        Self { meta, data }
526    }
527}
528
529// Display impls the annotated dump's JSON carries instead of serde derives on this
530// vocabulary, so the wire enums do not become frozen public JSON API.
531
532impl Display for StreamType {
533    /// `present`, `data`, `data[fsst]`, `offset[string]`, `length[rings]`.
534    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
535        match self {
536            Self::Present => f.write_str("present"),
537            Self::Data(dict) => {
538                let name = match dict {
539                    DictionaryType::None => return f.write_str("data"),
540                    DictionaryType::Single => "single",
541                    DictionaryType::Shared => "shared",
542                    DictionaryType::Vertex => "vertex",
543                    DictionaryType::Morton => "morton",
544                    DictionaryType::Fsst => "fsst",
545                };
546                write!(f, "data[{name}]")
547            }
548            Self::Offset(offset) => {
549                let name = match offset {
550                    OffsetType::Vertex => "vertex",
551                    OffsetType::Index => "index",
552                    OffsetType::String => "string",
553                    OffsetType::Key => "key",
554                };
555                write!(f, "offset[{name}]")
556            }
557            Self::Length(length) => {
558                let name = match length {
559                    LengthType::VarBinary => "var-binary",
560                    LengthType::Geometries => "geometries",
561                    LengthType::Parts => "parts",
562                    LengthType::Rings => "rings",
563                    LengthType::Triangles => "triangles",
564                    LengthType::Symbol => "symbol",
565                    LengthType::Dictionary => "dictionary",
566                    #[cfg(feature = "unstable-v2")]
567                    LengthType::Nested => "nested",
568                };
569                write!(f, "length[{name}]")
570            }
571        }
572    }
573}
574
575impl Display for LogicalEncoding {
576    /// The value kind, then its encoding: `int/rle`, `float/alp`, `vertex/morton-delta`.
577    ///
578    /// Run parameters are left out, they belong to the region's own annotation.
579    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
580        let (kind, enc) = match self {
581            Self::Int(int) => (
582                "int",
583                match int {
584                    IntLogical::None => "none",
585                    IntLogical::Delta => "delta",
586                    IntLogical::Rle(_) => "rle",
587                    IntLogical::DeltaRle(_) => "delta-rle",
588                    IntLogical::Delta2 => "delta2",
589                },
590            ),
591            Self::Bool(b) => (
592                "bool",
593                match b {
594                    BoolLogical::None => "none",
595                    BoolLogical::ByteRle(_) => "byte-rle",
596                },
597            ),
598            Self::Float(float) => (
599                "float",
600                match float {
601                    FloatLogical::None => "none",
602                    FloatLogical::Dict => "dict",
603                    FloatLogical::Alp(_) => "alp",
604                },
605            ),
606            Self::Vertex(vertex) => (
607                "vertex",
608                match vertex {
609                    VertexLogical::None => "none",
610                    VertexLogical::Delta => "delta",
611                    VertexLogical::ComponentwiseDelta => "componentwise-delta",
612                    VertexLogical::ComponentwiseDelta2 => "componentwise-delta2",
613                    VertexLogical::MortonDelta(_) => "morton-delta",
614                    VertexLogical::Rans => "rans",
615                    #[cfg(feature = "unstable-v2")]
616                    VertexLogical::Xyz(_, XyzLogical::None) => "xyz-none",
617                    #[cfg(feature = "unstable-v2")]
618                    VertexLogical::Xyz(_, XyzLogical::Delta) => "xyz-delta",
619                    #[cfg(feature = "unstable-v2")]
620                    VertexLogical::Xyz(_, XyzLogical::ComponentwiseDelta) => {
621                        "xyz-componentwise-delta"
622                    }
623                },
624            ),
625        };
626        write!(f, "{kind}/{enc}")
627    }
628}
629
630impl Display for PhysicalEncoding {
631    /// `none`, `varint`, `fastpfor[256be]`, `bit-packed`.
632    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
633        match self {
634            Self::None => f.write_str("none"),
635            Self::VarInt => f.write_str("varint"),
636            Self::FastPFor(kind) => {
637                let name = match kind {
638                    FastPForKind::Block256Be => "256be",
639                    #[cfg(feature = "unstable-v2")]
640                    FastPForKind::Block128Le => "128le",
641                };
642                write!(f, "fastpfor[{name}]")
643            }
644            #[cfg(feature = "unstable-v2")]
645            Self::BitPacked => f.write_str("bit-packed"),
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use rstest::rstest;
653    use usize_cast::IntoUsize as _;
654
655    use super::*;
656
657    fn morton() -> Morton {
658        Morton { bits: 4, shift: 0 }
659    }
660
661    fn alp() -> Alp {
662        Alp {
663            scale: AlpScale { e: 3, f: 1 },
664            base: -5,
665        }
666    }
667
668    fn rle() -> RleMeta {
669        RleMeta::Split {
670            runs: 2,
671            num_rle_values: 5,
672        }
673    }
674
675    #[rstest]
676    #[case::present(StreamType::Present, "present")]
677    #[case::data_none(StreamType::Data(DictionaryType::None), "data")]
678    #[case::data_single(StreamType::Data(DictionaryType::Single), "data[single]")]
679    #[case::data_shared(StreamType::Data(DictionaryType::Shared), "data[shared]")]
680    #[case::data_vertex(StreamType::Data(DictionaryType::Vertex), "data[vertex]")]
681    #[case::data_morton(StreamType::Data(DictionaryType::Morton), "data[morton]")]
682    #[case::data_fsst(StreamType::Data(DictionaryType::Fsst), "data[fsst]")]
683    #[case::offset_vertex(StreamType::Offset(OffsetType::Vertex), "offset[vertex]")]
684    #[case::offset_index(StreamType::Offset(OffsetType::Index), "offset[index]")]
685    #[case::offset_string(StreamType::Offset(OffsetType::String), "offset[string]")]
686    #[case::offset_key(StreamType::Offset(OffsetType::Key), "offset[key]")]
687    #[case::length_var_binary(StreamType::Length(LengthType::VarBinary), "length[var-binary]")]
688    #[case::length_geometries(StreamType::Length(LengthType::Geometries), "length[geometries]")]
689    #[case::length_parts(StreamType::Length(LengthType::Parts), "length[parts]")]
690    #[case::length_rings(StreamType::Length(LengthType::Rings), "length[rings]")]
691    #[case::length_triangles(StreamType::Length(LengthType::Triangles), "length[triangles]")]
692    #[case::length_symbol(StreamType::Length(LengthType::Symbol), "length[symbol]")]
693    #[case::length_dictionary(StreamType::Length(LengthType::Dictionary), "length[dictionary]")]
694    fn every_stream_type_renders_its_wire_label(
695        #[case] stream_type: StreamType,
696        #[case] expected: &str,
697    ) {
698        assert_eq!(stream_type.to_string(), expected);
699    }
700
701    #[cfg(feature = "unstable-v2")]
702    #[test]
703    fn a_nested_length_stream_renders_its_wire_label() {
704        assert_eq!(
705            StreamType::Length(LengthType::Nested).to_string(),
706            "length[nested]"
707        );
708    }
709
710    #[rstest]
711    #[case::int_none(LogicalEncoding::Int(IntLogical::None), "int/none")]
712    #[case::int_delta(LogicalEncoding::Int(IntLogical::Delta), "int/delta")]
713    #[case::int_rle(LogicalEncoding::Int(IntLogical::Rle(rle())), "int/rle")]
714    #[case::int_delta_rle(LogicalEncoding::Int(IntLogical::DeltaRle(rle())), "int/delta-rle")]
715    #[case::bool_none(LogicalEncoding::Bool(BoolLogical::None), "bool/none")]
716    #[case::bool_byte_rle(LogicalEncoding::Bool(BoolLogical::ByteRle(rle())), "bool/byte-rle")]
717    #[case::float_none(LogicalEncoding::Float(FloatLogical::None), "float/none")]
718    #[case::float_dict(LogicalEncoding::Float(FloatLogical::Dict), "float/dict")]
719    #[case::float_alp(LogicalEncoding::Float(FloatLogical::Alp(alp())), "float/alp")]
720    #[case::vertex_none(LogicalEncoding::Vertex(VertexLogical::None), "vertex/none")]
721    #[case::vertex_delta(LogicalEncoding::Vertex(VertexLogical::Delta), "vertex/delta")]
722    #[case::vertex_componentwise_delta(
723        LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta),
724        "vertex/componentwise-delta"
725    )]
726    #[case::vertex_morton_delta(
727        LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())),
728        "vertex/morton-delta"
729    )]
730    fn every_logical_encoding_renders_kind_then_encoding(
731        #[case] encoding: LogicalEncoding,
732        #[case] expected: &str,
733    ) {
734        assert_eq!(encoding.to_string(), expected);
735    }
736
737    #[cfg(feature = "unstable-v2")]
738    #[rstest]
739    #[case::none(XyzLogical::None, "vertex/xyz-none")]
740    #[case::delta(XyzLogical::Delta, "vertex/xyz-delta")]
741    #[case::componentwise_delta(XyzLogical::ComponentwiseDelta, "vertex/xyz-componentwise-delta")]
742    fn every_xyz_encoding_renders_kind_then_encoding(
743        #[case] xyz: XyzLogical,
744        #[case] expected: &str,
745    ) {
746        let step = ZStep::new(0).unwrap();
747        assert_eq!(
748            LogicalEncoding::Vertex(VertexLogical::Xyz(step, xyz)).to_string(),
749            expected
750        );
751    }
752
753    #[rstest]
754    #[case::none(PhysicalEncoding::None, "none")]
755    #[case::varint(PhysicalEncoding::VarInt, "varint")]
756    #[case::fastpfor_256be(
757        PhysicalEncoding::FastPFor(FastPForKind::Block256Be),
758        "fastpfor[256be]"
759    )]
760    fn every_physical_encoding_renders_its_wire_label(
761        #[case] encoding: PhysicalEncoding,
762        #[case] expected: &str,
763    ) {
764        assert_eq!(encoding.to_string(), expected);
765    }
766
767    #[cfg(feature = "unstable-v2")]
768    #[rstest]
769    #[case::fastpfor_128le(
770        PhysicalEncoding::FastPFor(FastPForKind::Block128Le),
771        "fastpfor[128le]"
772    )]
773    #[case::bit_packed(PhysicalEncoding::BitPacked, "bit-packed")]
774    fn every_v2_physical_encoding_renders_its_wire_label(
775        #[case] encoding: PhysicalEncoding,
776        #[case] expected: &str,
777    ) {
778        assert_eq!(encoding.to_string(), expected);
779    }
780
781    #[rstest]
782    #[case::ints(LogicalEncoding::Int(IntLogical::Delta), 6, 6)]
783    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::ComponentwiseDelta), 6, 3)]
784    #[case::morton_codes(LogicalEncoding::Vertex(VertexLogical::MortonDelta(morton())), 6, 6)]
785    #[cfg_attr(feature = "unstable-v2", case::triples(
786        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
787        6,
788        2
789    ))]
790    fn a_stream_counts_its_words_as_values(
791        #[case] logical: LogicalEncoding,
792        #[case] words: usize,
793        #[case] values: u32,
794    ) {
795        let meta = StreamMeta::new2(
796            StreamType::Present,
797            logical,
798            PhysicalEncoding::VarInt,
799            words,
800        )
801        .unwrap();
802        assert_eq!(meta.num_values, values);
803        assert_eq!(meta.num_words().unwrap().into_usize(), words);
804    }
805
806    #[rstest]
807    #[case::pairs(LogicalEncoding::Vertex(VertexLogical::Delta), 5, 2)]
808    #[cfg_attr(feature = "unstable-v2", case::triples(
809        LogicalEncoding::Vertex(VertexLogical::Xyz(ZStep::new(0).unwrap(), XyzLogical::Delta)),
810        4,
811        3
812    ))]
813    fn a_stream_of_partial_vertices_is_rejected(
814        #[case] logical: LogicalEncoding,
815        #[case] words: u32,
816        #[case] per_vertex: u32,
817    ) {
818        let err = StreamMeta::new2(
819            StreamType::Present,
820            logical,
821            PhysicalEncoding::VarInt,
822            words.into_usize(),
823        )
824        .unwrap_err();
825        assert!(
826            matches!(err, MltError::PartialVertex(w, p) if w == words && p == per_vertex),
827            "{err:?}"
828        );
829    }
830
831    #[rstest]
832    #[case::int(LogicalEncoding::Int(IntLogical::None), ValueKind::Int)]
833    #[case::bool(LogicalEncoding::Bool(BoolLogical::None), ValueKind::Bool)]
834    #[case::float(LogicalEncoding::Float(FloatLogical::None), ValueKind::Float)]
835    #[case::vertex(LogicalEncoding::Vertex(VertexLogical::None), ValueKind::Vertex)]
836    fn the_identity_encoding_of_a_kind_reports_that_kind_back(
837        #[case] expected: LogicalEncoding,
838        #[case] kind: ValueKind,
839    ) {
840        let none = LogicalEncoding::none(kind);
841        assert_eq!(none, expected);
842        assert_eq!(none.kind(), kind);
843        assert!(none.is_identity());
844    }
845}