ultrapacker/
lib.rs

1use std::ops::Deref;
2
3// Was curious to see if there was a way to utilize the wasted space that a lot of
4// compacting runs into. If values don't match up with a power of two there is inherent waste.
5//
6// Combining values seems like a common way to get around this. Kind of see it a bit like
7// arithmetic encoding or linearization of multidimensional arrays.
8//
9// e.g.
10//
11// 3d array of 18x18x18 will be 5832 elements, indexing this via each axis like:
12// - needs 2^5, but that wastes 14 values per axis, 43.75% of the space allocated to it.
13// indexing it via a simple linearization scheme: x + y * 18 + z * 18 * 18
14// means we only need a number for `5832` which requires 13 bits savings 2 bits.
15//
16// https://save-buffer.github.io/ultrapack.html
17
18pub const fn optimal_bundle_size(max_value: u64) -> BundleSize {
19    let naive_bits = if max_value == 0 {
20        return BundleSize(0);
21    } else {
22        max_value.ilog2() + 1
23    };
24
25    let mut best_size = 1u8;
26    let mut best_bits_per_val = naive_bits as f64;
27
28    // test bundle sizes until we overflow u64
29    let mut bundle_size = 1;
30    while bundle_size <= 40u8 {
31        // max_value^k - 1
32        let Some(max_bundle) = max_value.checked_pow(bundle_size as u32) else {
33            break;
34        };
35
36        let bits_needed = (64 - (max_bundle - 1).leading_zeros()) as u8;
37        let bits_per_val = bits_needed as f64 / bundle_size as f64;
38
39        if bits_per_val < best_bits_per_val {
40            best_bits_per_val = bits_per_val;
41            best_size = bundle_size;
42        }
43
44        bundle_size += 1;
45    }
46
47    BundleSize(best_size)
48}
49
50pub const fn bits_per_bundle(max_value: u64, bundle_size: BundleSize) -> BitsPerBundle {
51    let max_bundle = max_value.pow(bundle_size.0 as u32);
52    BitsPerBundle((64 - (max_bundle - 1).leading_zeros()) as u8)
53}
54
55#[derive(Copy, Clone, Debug)]
56pub struct BundleSize(pub u8);
57impl Deref for BundleSize {
58    type Target = u8;
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64#[derive(Copy, Clone, Debug)]
65pub struct BitsPerBundle(pub u8);
66impl Deref for BitsPerBundle {
67    type Target = u8;
68    fn deref(&self) -> &Self::Target {
69        &self.0
70    }
71}
72
73/// theoretical minimum bits per value
74pub fn theoretical_bits(max_value: u64) -> f64 {
75    (max_value as f64).log2()
76}
77
78/// traditional bitpacking bits per value
79pub fn naive_bits(max_value: u64) -> u8 {
80    (64 - (max_value - 1).leading_zeros()) as u8
81}
82
83pub fn encode(bundle_size: BundleSize, max_value: u64, values: &[u64]) -> u64 {
84    assert_eq!(values.len(), *bundle_size as usize);
85
86    let mut bundle: u64 = 0;
87    for &val in values {
88        assert!(val < max_value);
89        bundle = bundle * max_value + val;
90    }
91    bundle
92}
93
94pub fn decode(bundle_size: BundleSize, max_value: u64, mut bundle: u64) -> Vec<u64> {
95    let mut values = vec![0u64; *bundle_size as usize];
96
97    for i in (0..*bundle_size as usize).rev() {
98        values[i] = bundle % max_value;
99        bundle /= max_value;
100    }
101
102    values
103}