cloudini 0.3.1

The cloudini point cloud compression library for Rust.
Documentation
//! Per-field decoding logic for the Cloudini format.
//!
//! Each [`FieldDecoder`] variant is the inverse of its corresponding
//! [`crate::field_encoder::FieldEncoder`] variant. Decoders carry the same
//! delta/XOR state and must be [`reset`](FieldDecoder::reset) at the start of
//! every chunk.

use crate::types::FieldType;
use crate::varint::decode_varint;

/// Write the lower `size` bytes of `value` (little-endian) into `point` at `offset`.
fn write_int_to_point(point: &mut [u8], offset: usize, value: i64, size: usize) {
    let bytes = value.to_le_bytes();
    point[offset..offset + size].copy_from_slice(&bytes[..size]);
}

/// A stateful decoder for one point field (or a batch of consecutive float fields).
///
/// Call [`FieldDecoder::reset`] at the start of each chunk to clear delta state.
/// Call [`FieldDecoder::decode`] once per point.
pub enum FieldDecoder {
    /// Raw byte copy — used for INT8/UINT8 and unquantised floats.
    Copy { offset: usize, size: usize },
    /// Varint delta decoder for integer fields.
    Int {
        offset: usize,
        /// Byte width to write back (1/2/4/8).
        size: usize,
        prev: i64,
    },
    /// Inverse of [`crate::field_encoder::FieldEncoder::Float32Lossy`].
    Float32Lossy {
        offset: usize,
        /// `resolution` value — multiply the decoded integer to recover the float.
        multiplier: f32,
        prev: i64,
    },
    /// Inverse of [`crate::field_encoder::FieldEncoder::Float64Lossy`].
    Float64Lossy {
        offset: usize,
        multiplier: f64,
        prev: i64,
    },
    /// Inverse of [`crate::field_encoder::FieldEncoder::Float64Xor`].
    Float64Xor { offset: usize, prev_bits: u64 },
    /// Batch decoder for 3 or 4 consecutive FLOAT32 fields.
    FloatNLossy {
        offsets: [usize; 4],
        /// `resolution[i]` for each component.
        multipliers: [f32; 4],
        prev: [i32; 4],
        /// 3 or 4
        count: usize,
    },
}

impl FieldDecoder {
    /// Reset delta/XOR state. Must be called at the start of every chunk.
    pub fn reset(&mut self) {
        match self {
            FieldDecoder::Int { prev, .. } => *prev = 0,
            FieldDecoder::Float32Lossy { prev, .. } => *prev = 0,
            FieldDecoder::Float64Lossy { prev, .. } => *prev = 0,
            FieldDecoder::Float64Xor { prev_bits, .. } => *prev_bits = 0,
            FieldDecoder::FloatNLossy { prev, .. } => *prev = [0i32; 4],
            FieldDecoder::Copy { .. } => {}
        }
    }

    /// Lower bound on encoded input bytes consumed per call (for bounds checking).
    pub fn min_input_bytes(&self) -> usize {
        match self {
            FieldDecoder::Copy { size, .. } => *size,
            FieldDecoder::Int { .. } => 1,
            FieldDecoder::Float32Lossy { .. } => 1,
            FieldDecoder::Float64Lossy { .. } => 1,
            FieldDecoder::Float64Xor { .. } => 8,
            FieldDecoder::FloatNLossy { count, .. } => *count,
        }
    }

    /// Decode from `input` and write the reconstructed value(s) into `point`.
    ///
    /// Returns the number of bytes consumed from `input`.
    ///
    /// # Errors
    ///
    /// Returns an error if `input` is truncated or contains an invalid varint.
    pub fn decode(&mut self, input: &[u8], point: &mut [u8]) -> crate::Result<usize> {
        match self {
            FieldDecoder::Copy { offset, size } => {
                point[*offset..*offset + *size].copy_from_slice(&input[..*size]);
                Ok(*size)
            }

            FieldDecoder::Int { offset, size, prev } => {
                let (diff, consumed) = decode_varint(input)?;
                let value = *prev + diff;
                *prev = value;
                write_int_to_point(point, *offset, value, *size);
                Ok(consumed)
            }

            FieldDecoder::Float32Lossy {
                offset,
                multiplier,
                prev,
            } => {
                if input[0] == 0 {
                    // NaN sentinel
                    point[*offset..*offset + 4].copy_from_slice(&f32::NAN.to_le_bytes());
                    *prev = 0;
                    return Ok(1);
                }
                let (diff, consumed) = decode_varint(input)?;
                let value = *prev + diff;
                *prev = value;
                let float_val = value as f32 * *multiplier;
                point[*offset..*offset + 4].copy_from_slice(&float_val.to_le_bytes());
                Ok(consumed)
            }

            FieldDecoder::Float64Lossy {
                offset,
                multiplier,
                prev,
            } => {
                if input[0] == 0 {
                    point[*offset..*offset + 8].copy_from_slice(&f64::NAN.to_le_bytes());
                    *prev = 0;
                    return Ok(1);
                }
                let (diff, consumed) = decode_varint(input)?;
                let value = *prev + diff;
                *prev = value;
                let float_val = value as f64 * *multiplier;
                point[*offset..*offset + 8].copy_from_slice(&float_val.to_le_bytes());
                Ok(consumed)
            }

            FieldDecoder::Float64Xor { offset, prev_bits } => {
                let residual = u64::from_le_bytes(input[..8].try_into().unwrap());
                let current = residual ^ *prev_bits;
                *prev_bits = current;
                point[*offset..*offset + 8].copy_from_slice(&current.to_le_bytes());
                Ok(8)
            }

            FieldDecoder::FloatNLossy {
                offsets,
                multipliers,
                prev,
                count,
            } => {
                let n = *count;
                let mut consumed = 0;

                for i in 0..n {
                    if consumed >= input.len() {
                        return Err(crate::Error::Truncated("FloatNLossy: truncated".into()));
                    }
                    let b0 = input[consumed];
                    if b0 == 0 {
                        // NaN sentinel
                        prev[i] = 0;
                        point[offsets[i]..offsets[i] + 4].copy_from_slice(&f32::NAN.to_le_bytes());
                        consumed += 1;
                    } else if b0 & 0x80 == 0 {
                        // Fast-path: 1-byte varint — common case for small coordinate deltas.
                        // Inlined here to avoid subslice creation and function call overhead.
                        let uval = (b0 as u32) - 1;
                        let diff = ((uval >> 1) as i32) ^ -((uval & 1) as i32);
                        let new_val = diff.wrapping_add(prev[i]);
                        prev[i] = new_val;
                        let float_val = new_val as f32 * multipliers[i];
                        point[offsets[i]..offsets[i] + 4].copy_from_slice(&float_val.to_le_bytes());
                        consumed += 1;
                    } else {
                        // Slow path: multi-byte varint.
                        let (diff, n_bytes) = decode_varint(&input[consumed..])?;
                        // Truncate i64 diff to i32 then add (matches reference i32 accumulator)
                        let new_val = (diff as i32).wrapping_add(prev[i]);
                        prev[i] = new_val;
                        let float_val = new_val as f32 * multipliers[i];
                        point[offsets[i]..offsets[i] + 4].copy_from_slice(&float_val.to_le_bytes());
                        consumed += n_bytes;
                    }
                }
                Ok(consumed)
            }
        }
    }
}

/// Build the ordered list of field decoders for a given set of fields and encoding mode.
///
/// Mirrors [`crate::field_encoder::build_encoders`] exactly so that the decoder
/// consumes bytes in the same order the encoder produced them.
pub fn build_decoders(
    fields: &[crate::types::PointField],
    encoding_opt: crate::types::EncodingOptions,
) -> Vec<FieldDecoder> {
    use crate::types::EncodingOptions;

    let mut decoders: Vec<FieldDecoder> = Vec::new();
    let mut start_index = 0;

    if encoding_opt == EncodingOptions::None {
        for field in fields {
            decoders.push(FieldDecoder::Copy {
                offset: field.offset as usize,
                size: field.field_type.size_of(),
            });
        }
        return decoders;
    }

    // FloatNLossy for the first run of FLOAT32+resolution fields (3 or 4 only)
    if encoding_opt == EncodingOptions::Lossy {
        let floats_count = fields
            .iter()
            .take_while(|f| f.field_type == FieldType::Float32 && f.resolution.is_some())
            .count();

        if floats_count == 3 || floats_count == 4 {
            let mut offsets = [0usize; 4];
            let mut multipliers = [0.0f32; 4];
            for i in 0..floats_count {
                offsets[i] = fields[i].offset as usize;
                multipliers[i] = fields[i].resolution.unwrap(); // resolution: int * res → float
            }
            decoders.push(FieldDecoder::FloatNLossy {
                offsets,
                multipliers,
                prev: [0i32; 4],
                count: floats_count,
            });
            start_index = floats_count;
        }
    }

    for field in &fields[start_index..] {
        let offset = field.offset as usize;
        let decoder = match field.field_type {
            FieldType::Float32 => {
                if let Some(res) = field.resolution {
                    FieldDecoder::Float32Lossy {
                        offset,
                        multiplier: res,
                        prev: 0,
                    }
                } else {
                    FieldDecoder::Copy { offset, size: 4 }
                }
            }
            FieldType::Float64 => {
                if let Some(res) = field.resolution {
                    FieldDecoder::Float64Lossy {
                        offset,
                        multiplier: res as f64,
                        prev: 0,
                    }
                } else {
                    FieldDecoder::Float64Xor {
                        offset,
                        prev_bits: 0,
                    }
                }
            }
            FieldType::Int8 | FieldType::Uint8 => FieldDecoder::Copy { offset, size: 1 },
            FieldType::Int16 => FieldDecoder::Int {
                offset,
                size: 2,
                prev: 0,
            },
            FieldType::Uint16 => FieldDecoder::Int {
                offset,
                size: 2,
                prev: 0,
            },
            FieldType::Int32 => FieldDecoder::Int {
                offset,
                size: 4,
                prev: 0,
            },
            FieldType::Uint32 => FieldDecoder::Int {
                offset,
                size: 4,
                prev: 0,
            },
            FieldType::Int64 => FieldDecoder::Int {
                offset,
                size: 8,
                prev: 0,
            },
            FieldType::Uint64 => FieldDecoder::Int {
                offset,
                size: 8,
                prev: 0,
            },
            _ => panic!("Unsupported field type in decoder"),
        };
        decoders.push(decoder);
    }

    decoders
}