Skip to main content

mtb_align/
bitmap.rs

1use crate::strips::fill_rows;
2
3/// Bits per word. Ward counts 32 or 64 at a time; this crate always takes 64.
4pub(crate) const WORD_BITS: usize = u64::BITS as usize;
5
6/// A one-bit-per-pixel image, least-significant bit first, each row padded to a
7/// word boundary so a shift never crosses a row. Ward flattens it instead.
8#[derive(Clone, PartialEq, Eq)]
9pub struct Bitmap {
10    words: Vec<u64>,
11    width: usize,
12    height: usize,
13    words_per_row: usize,
14}
15
16impl Bitmap {
17    pub fn zeros(width: usize, height: usize) -> Self {
18        let words_per_row = width.div_ceil(WORD_BITS);
19        Self {
20            words: vec![0; words_per_row * height],
21            width,
22            height,
23            words_per_row,
24        }
25    }
26
27    pub fn width(&self) -> usize {
28        self.width
29    }
30
31    pub fn height(&self) -> usize {
32        self.height
33    }
34
35    /// Panics unless the coordinate is inside the bitmap.
36    pub fn get(&self, x: usize, y: usize) -> bool {
37        let (word, bit) = self.locate(x, y);
38        self.words[word] >> bit & 1 == 1
39    }
40
41    /// Panics unless the coordinate is inside the bitmap.
42    pub fn set(&mut self, x: usize, y: usize, value: bool) {
43        let (word, bit) = self.locate(x, y);
44        if value {
45            self.words[word] |= 1 << bit;
46        } else {
47            self.words[word] &= !(1 << bit);
48        }
49    }
50
51    /// Ward's `BitmapTotal`. Padding is held clear, so whole words can count.
52    pub fn count_ones(&self) -> u64 {
53        self.words.iter().map(|word| word.count_ones() as u64).sum()
54    }
55
56    /// Builds a bitmap a word at a time. Setting single bits costs a bounds check
57    /// and a read-modify-write each, which was most of the alignment.
58    pub(crate) fn packed(
59        width: usize,
60        height: usize,
61        bit: impl Fn(usize, usize) -> bool + Send + Sync,
62    ) -> Self {
63        let mut bitmap = Self::zeros(width, height);
64        let words_per_row = bitmap.words_per_row;
65
66        fill_rows(&mut bitmap.words, words_per_row, |y, row| {
67            for (index, word) in row.iter_mut().enumerate() {
68                let base = index * WORD_BITS;
69                let span = WORD_BITS.min(width - base); // stops at the width
70
71                let mut packed = 0;
72                for offset in 0..span {
73                    packed |= (bit(base + offset, y) as u64) << offset;
74                }
75
76                *word = packed;
77            }
78        });
79
80        bitmap
81    }
82
83    /// How many words each row occupies, padding included.
84    pub fn words_per_row(&self) -> usize {
85        self.words_per_row
86    }
87
88    pub fn row(&self, y: usize) -> &[u64] {
89        &self.words[y * self.words_per_row..(y + 1) * self.words_per_row]
90    }
91
92    fn locate(&self, x: usize, y: usize) -> (usize, usize) {
93        assert!(
94            x < self.width && y < self.height,
95            "({x}, {y}) is outside a {}x{} bitmap",
96            self.width,
97            self.height
98        );
99        (y * self.words_per_row + x / WORD_BITS, x % WORD_BITS)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use proptest::prelude::*;
107
108    #[test]
109    fn a_new_bitmap_holds_no_bits() {
110        let bitmap = Bitmap::zeros(100, 40);
111
112        assert_eq!(bitmap.width(), 100);
113        assert_eq!(bitmap.height(), 40);
114        assert_eq!(bitmap.count_ones(), 0);
115    }
116
117    #[test]
118    fn a_set_bit_reads_back() {
119        let mut bitmap = Bitmap::zeros(100, 40);
120
121        bitmap.set(70, 3, true);
122
123        assert!(bitmap.get(70, 3));
124        assert!(!bitmap.get(69, 3));
125        assert!(!bitmap.get(70, 2));
126        assert_eq!(bitmap.count_ones(), 1);
127    }
128
129    #[test]
130    fn clearing_a_bit_removes_it() {
131        let mut bitmap = Bitmap::zeros(8, 2);
132
133        bitmap.set(5, 1, true);
134        bitmap.set(5, 1, false);
135
136        assert!(!bitmap.get(5, 1));
137        assert_eq!(bitmap.count_ones(), 0);
138    }
139
140    #[test]
141    fn the_padding_past_the_width_never_counts() {
142        let mut bitmap = Bitmap::zeros(3, 4);
143
144        for y in 0..4 {
145            for x in 0..3 {
146                bitmap.set(x, y, true);
147            }
148        }
149
150        assert_eq!(bitmap.count_ones(), 12);
151    }
152
153    #[test]
154    fn each_row_starts_on_a_word_boundary() {
155        let bitmap = Bitmap::zeros(3, 4);
156
157        assert_eq!(bitmap.words_per_row(), 1);
158        assert_eq!(bitmap.row(0).len(), 1);
159        assert_eq!(bitmap.row(3).len(), 1);
160    }
161
162    #[test]
163    fn a_row_wider_than_one_word_spans_several() {
164        let mut bitmap = Bitmap::zeros(130, 2);
165
166        assert_eq!(bitmap.words_per_row(), 3);
167
168        bitmap.set(0, 0, true);
169        bitmap.set(64, 0, true);
170        bitmap.set(129, 0, true);
171
172        assert_eq!(bitmap.row(0), &[1, 1, 1 << 1]);
173        assert_eq!(bitmap.count_ones(), 3);
174    }
175
176    #[test]
177    #[should_panic(expected = "(3, 0) is outside a 3x4 bitmap")]
178    fn reading_past_the_width_panics() {
179        Bitmap::zeros(3, 4).get(3, 0);
180    }
181
182    #[test]
183    #[should_panic(expected = "(0, 4) is outside a 3x4 bitmap")]
184    fn writing_past_the_height_panics() {
185        Bitmap::zeros(3, 4).set(0, 4, true);
186    }
187
188    proptest! {
189        /// A word running past a row's end would leave padding set and counted.
190        #[test]
191        fn packing_a_word_at_a_time_matches_setting_bit_by_bit(
192            width in 1usize..200,
193            height in 1usize..8,
194            seed in any::<u64>(),
195        ) {
196            let bit = |x: usize, y: usize| {
197                (seed ^ (x as u64) << 7 ^ (y as u64)).wrapping_mul(0x2545F491_4F6CDD1D).count_ones().is_multiple_of(3)
198            };
199
200            let mut one_at_a_time = Bitmap::zeros(width, height);
201            for y in 0..height {
202                for x in 0..width {
203                    one_at_a_time.set(x, y, bit(x, y));
204                }
205            }
206
207            prop_assert!(Bitmap::packed(width, height, bit) == one_at_a_time);
208        }
209
210        /// Slack depends on `width % 64`, so this has to hold at every width.
211        #[test]
212        fn the_count_matches_the_bits_actually_set(
213            width in 1usize..200,
214            height in 1usize..20,
215            positions in prop::collection::vec((0usize..200, 0usize..20), 0..50),
216        ) {
217            let mut bitmap = Bitmap::zeros(width, height);
218            let inside: std::collections::BTreeSet<_> = positions
219                .into_iter()
220                .filter(|&(x, y)| x < width && y < height)
221                .collect();
222
223            for &(x, y) in &inside {
224                bitmap.set(x, y, true);
225            }
226
227            prop_assert_eq!(bitmap.count_ones(), inside.len() as u64);
228            for &(x, y) in &inside {
229                prop_assert!(bitmap.get(x, y));
230            }
231        }
232
233        /// Proves rows do not overlap.
234        #[test]
235        fn setting_one_bit_leaves_every_other_clear(
236            width in 1usize..140,
237            height in 1usize..10,
238            index in 0usize..1400,
239        ) {
240            let (x, y) = (index % width, index / width % height);
241            let mut bitmap = Bitmap::zeros(width, height);
242
243            bitmap.set(x, y, true);
244
245            for probe_y in 0..height {
246                for probe_x in 0..width {
247                    prop_assert_eq!(bitmap.get(probe_x, probe_y), (probe_x, probe_y) == (x, y));
248                }
249            }
250        }
251    }
252}