Skip to main content

long_screen/engine/
pyramid.rs

1//! The preview pyramid of one canvas (docs/ARCHITECTURE.md §八): each level halves the one below it, four child tiles
2//! per parent tile, alpha-weighted so unobserved pixels never darken observed ones. Level 0 stays at source
3//! resolution and is never rewritten.
4//!
5//! [`PyramidSession`] builds one canvas's levels as a resumable job the host drives. The pyramid does not go through
6//! the tile cache: the host reads stored child tiles and writes parent tiles straight to and from storage. Per
7//! level, [`PyramidSession::step`] first asks for the stored tile index of the level below ([`Step::NeedIndex`]:
8//! the host passes every row to [`PyramidSession::queue`], which names the parent tile to queue), then works
9//! through the queued parents in storage-key order: [`Step::NeedChildren`] names the four child tiles to read
10//! ([`PyramidSession::provide`]), [`Step::Parent`] hands back the assembled parent (or `None` when no child exists)
11//! for the host to store before it drops the parent from its queue and checks for pause and stop.
12//! [`Step::LevelDone`] reports a finished level, [`Step::Done`] the last one.
13
14use crate::engine::tiles::TileKey;
15use crate::kernels::pyramid::assemble_parent;
16use std::collections::BTreeMap;
17
18/// Levels above level 0: how many times `size` doubles before it reaches the larger bound dimension, i.e.
19/// `ceil(log2(max(width, height) / size))` clamped to 0, counted by doubling so it never depends on a
20/// floating-point logarithm.
21pub fn level_count(width: f64, height: f64, size: usize) -> u32 {
22    let max_dimension = width.max(height);
23    let mut levels = 0;
24    let mut scale = size as f64;
25    while scale < max_dimension {
26        levels += 1;
27        scale *= 2.0;
28    }
29    levels
30}
31
32/// The storage-key suffix of a queued parent, `x_y`; the queue runs in the byte order of these keys, which is the
33/// order the host's store iterates them in.
34pub fn queue_key(x: i32, y: i32) -> String {
35    format!("{x}_{y}")
36}
37
38#[derive(Clone, Debug, PartialEq)]
39pub enum Step {
40    /// Pass every stored tile-index row of level `level - 1` to [`PyramidSession::queue`], then step again.
41    NeedIndex {
42        level: u32,
43    },
44    /// Read these four child tiles (quadrant order: `[dx=0,dy=0]`, `[1,0]`, `[0,1]`, `[1,1]`), pass their decoded
45    /// pixels (or `None` for one that is not stored) to [`PyramidSession::provide`], then step again.
46    NeedChildren {
47        level: u32,
48        x: i32,
49        y: i32,
50        children: [TileKey; 4],
51    },
52    /// One queued parent is done: store `pixels` (`size × size` RGBA) as tile `x_y` of `level` when there are any,
53    /// then drop the parent from the queue and step again.
54    Parent {
55        level: u32,
56        x: i32,
57        y: i32,
58        pixels: Option<Vec<u8>>,
59    },
60    /// Every parent of `level` is done: the canvas now has `level` as its top level.
61    LevelDone {
62        level: u32,
63    },
64    Done,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum PyramidError {
69    /// The tile size is not a positive even number.
70    BadSize,
71    /// `queue` outside [`Step::NeedIndex`], or `provide` outside [`Step::NeedChildren`].
72    Unexpected,
73    /// A provided child does not hold `size × size` RGBA pixels.
74    BadChild,
75}
76
77#[derive(Clone, Debug, PartialEq)]
78enum Phase {
79    /// Next step asks for the index of the level below `level`.
80    Start(u32),
81    /// The host is passing index rows for `level`.
82    Indexing(u32),
83    /// Working through the queue of `level`.
84    Parents(u32),
85    /// The children of the parent at the queue's head were asked for.
86    Children(u32, i32, i32),
87    /// The children of that parent were provided.
88    Provided(u32, i32, i32),
89    Done,
90}
91
92/// One canvas's pyramid in progress.
93pub struct PyramidSession {
94    canvas: String,
95    size: usize,
96    levels: u32,
97    phase: Phase,
98    queue: BTreeMap<String, (i32, i32)>,
99    children: [Option<Vec<u8>>; 4],
100}
101
102impl PyramidSession {
103    /// A pyramid for canvas `canvas` whose bounds are `width × height`, over `size`-pixel tiles.
104    pub fn new(canvas: &str, width: f64, height: f64, size: usize) -> Result<Self, PyramidError> {
105        if size == 0 || !size.is_multiple_of(2) {
106            return Err(PyramidError::BadSize);
107        }
108        let levels = level_count(width, height, size);
109        Ok(Self {
110            canvas: canvas.to_string(),
111            size,
112            levels,
113            phase: if levels == 0 {
114                Phase::Done
115            } else {
116                Phase::Start(1)
117            },
118            queue: BTreeMap::new(),
119            children: [None, None, None, None],
120        })
121    }
122
123    /// Levels above level 0 this canvas gets.
124    pub fn levels(&self) -> u32 {
125        self.levels
126    }
127
128    /// One stored index row `(x, y)` of the level below: queues its parent and returns the parent's coordinates
129    /// (the host records the queued parent under them).
130    pub fn queue(&mut self, x: i32, y: i32) -> Result<(i32, i32), PyramidError> {
131        if !matches!(self.phase, Phase::Indexing(_)) {
132            return Err(PyramidError::Unexpected);
133        }
134        let parent = (x.div_euclid(2), y.div_euclid(2));
135        self.queue.insert(queue_key(parent.0, parent.1), parent);
136        Ok(parent)
137    }
138
139    /// The decoded pixels of the four children [`Step::NeedChildren`] asked for, `None` for each one not stored.
140    pub fn provide(&mut self, children: [Option<Vec<u8>>; 4]) -> Result<(), PyramidError> {
141        let Phase::Children(level, x, y) = self.phase else {
142            return Err(PyramidError::Unexpected);
143        };
144        let n = self.size * self.size * 4;
145        if children.iter().flatten().any(|c| c.len() != n) {
146            return Err(PyramidError::BadChild);
147        }
148        self.children = children;
149        self.phase = Phase::Provided(level, x, y);
150        Ok(())
151    }
152
153    pub fn step(&mut self) -> Step {
154        match self.phase.clone() {
155            Phase::Start(level) => {
156                self.phase = Phase::Indexing(level);
157                Step::NeedIndex { level }
158            }
159            Phase::Indexing(level) | Phase::Parents(level) => match self.queue.pop_first() {
160                Some((_, (x, y))) => {
161                    self.phase = Phase::Children(level, x, y);
162                    let child = |dx: i32, dy: i32| {
163                        TileKey::new(&self.canvas, level - 1, x * 2 + dx, y * 2 + dy)
164                    };
165                    Step::NeedChildren {
166                        level,
167                        x,
168                        y,
169                        children: [child(0, 0), child(1, 0), child(0, 1), child(1, 1)],
170                    }
171                }
172                None => {
173                    self.phase = if level < self.levels {
174                        Phase::Start(level + 1)
175                    } else {
176                        Phase::Done
177                    };
178                    Step::LevelDone { level }
179                }
180            },
181            // Asked again without the children: ask again.
182            Phase::Children(level, x, y) => {
183                let child = |dx: i32, dy: i32| {
184                    TileKey::new(&self.canvas, level - 1, x * 2 + dx, y * 2 + dy)
185                };
186                Step::NeedChildren {
187                    level,
188                    x,
189                    y,
190                    children: [child(0, 0), child(1, 0), child(0, 1), child(1, 1)],
191                }
192            }
193            Phase::Provided(level, x, y) => {
194                self.phase = Phase::Parents(level);
195                let children = std::mem::take(&mut self.children);
196                let pixels = children.iter().any(Option::is_some).then(|| {
197                    let mut out = vec![0u8; self.size * self.size * 4];
198                    let [a, b, c, d] = &children;
199                    assemble_parent(
200                        [a.as_deref(), b.as_deref(), c.as_deref(), d.as_deref()],
201                        self.size,
202                        &mut out,
203                    );
204                    out
205                });
206                Step::Parent {
207                    level,
208                    x,
209                    y,
210                    pixels,
211                }
212            }
213            Phase::Done => Step::Done,
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    type Written = Vec<(u32, i32, i32, [u8; 4])>;
223
224    /// Drives a session to the end against `stored` level-0 tiles (opaque pixels of one value each), returning every
225    /// parent written as `(level, x, y, first pixel)`, the levels reported done, and the queued parents.
226    fn run(
227        size: usize,
228        width: f64,
229        height: f64,
230        level0: &[(i32, i32, u8)],
231    ) -> (Written, Vec<u32>, usize) {
232        let mut session = PyramidSession::new("c", width, height, size).unwrap();
233        let mut store: BTreeMap<(u32, i32, i32), Vec<u8>> = BTreeMap::new();
234        for &(x, y, v) in level0 {
235            let mut px = vec![v; size * size * 4];
236            px.iter_mut().skip(3).step_by(4).for_each(|a| *a = 255);
237            store.insert((0, x, y), px);
238        }
239        let (mut parents, mut done, mut queued) = (Vec::new(), Vec::new(), 0);
240        loop {
241            match session.step() {
242                Step::NeedIndex { level } => {
243                    // The store's index order: string keys `x_y`.
244                    let mut rows: Vec<(String, i32, i32)> = store
245                        .keys()
246                        .filter(|k| k.0 == level - 1)
247                        .map(|&(_, x, y)| (queue_key(x, y), x, y))
248                        .collect();
249                    rows.sort();
250                    for (_, x, y) in rows {
251                        session.queue(x, y).unwrap();
252                        queued += 1;
253                    }
254                }
255                Step::NeedChildren { children, .. } => {
256                    let got = children
257                        .clone()
258                        .map(|k| store.get(&(k.level, k.x, k.y)).cloned());
259                    session.provide(got).unwrap();
260                }
261                Step::Parent {
262                    level,
263                    x,
264                    y,
265                    pixels,
266                } => {
267                    if let Some(p) = pixels {
268                        parents.push((level, x, y, [p[0], p[1], p[2], p[3]]));
269                        store.insert((level, x, y), p);
270                    }
271                }
272                Step::LevelDone { level } => done.push(level),
273                Step::Done => return (parents, done, queued),
274            }
275        }
276    }
277
278    #[test]
279    fn level_count_matches_the_doubling_table() {
280        let size = 64;
281        for (width, expected) in [
282            (1.0, 0),
283            (63.0, 0),
284            (64.0, 0),
285            (65.0, 1),
286            (128.0, 1),
287            (129.0, 2),
288            (256.0, 2),
289            (257.0, 3),
290        ] {
291            assert_eq!(level_count(width, 1.0, size), expected, "width {width}");
292            assert_eq!(level_count(1.0, width, size), expected, "height {width}");
293        }
294    }
295
296    #[test]
297    fn a_canvas_within_one_tile_has_no_levels_and_asks_for_nothing() {
298        let mut s = PyramidSession::new("c", 10.0, 10.0, 64).unwrap();
299        assert_eq!(s.levels(), 0);
300        assert_eq!(s.step(), Step::Done);
301    }
302
303    #[test]
304    fn an_odd_or_zero_tile_size_is_refused() {
305        assert_eq!(
306            PyramidSession::new("c", 1.0, 1.0, 0).err(),
307            Some(PyramidError::BadSize)
308        );
309        assert_eq!(
310            PyramidSession::new("c", 1.0, 1.0, 15).err(),
311            Some(PyramidError::BadSize)
312        );
313    }
314
315    #[test]
316    fn queue_and_provide_out_of_turn_are_refused() {
317        let mut s = PyramidSession::new("c", 200.0, 10.0, 64).unwrap();
318        assert_eq!(s.queue(0, 0), Err(PyramidError::Unexpected));
319        assert_eq!(
320            s.provide([None, None, None, None]),
321            Err(PyramidError::Unexpected)
322        );
323        assert_eq!(s.step(), Step::NeedIndex { level: 1 });
324        assert_eq!(
325            s.provide([None, None, None, None]),
326            Err(PyramidError::Unexpected)
327        );
328        s.queue(0, 0).unwrap();
329        assert!(matches!(s.step(), Step::NeedChildren { .. }));
330        assert_eq!(s.queue(0, 0), Err(PyramidError::Unexpected));
331        assert_eq!(
332            s.provide([Some(vec![0; 3]), None, None, None]),
333            Err(PyramidError::BadChild)
334        );
335    }
336
337    #[test]
338    fn parents_floor_negative_coordinates_and_run_in_storage_key_order() {
339        let mut s = PyramidSession::new("c", 4096.0, 64.0, 64).unwrap();
340        assert_eq!(s.step(), Step::NeedIndex { level: 1 });
341        // -1 → -1 (floor, not truncation); 20 and 21 share parent 10; 2 → 1.
342        assert_eq!(s.queue(-1, 0).unwrap(), (-1, 0));
343        assert_eq!(s.queue(20, 0).unwrap(), (10, 0));
344        assert_eq!(s.queue(21, 1).unwrap(), (10, 0));
345        assert_eq!(s.queue(2, 0).unwrap(), (1, 0));
346        let mut order = Vec::new();
347        loop {
348            match s.step() {
349                Step::NeedChildren { x, y, children, .. } => {
350                    order.push((x, y));
351                    assert_eq!(children[1], TileKey::new("c", 0, x * 2 + 1, y * 2));
352                    assert_eq!(children[2], TileKey::new("c", 0, x * 2, y * 2 + 1));
353                    s.provide([None, None, None, None]).unwrap();
354                }
355                Step::Parent { pixels, .. } => assert!(pixels.is_none()),
356                Step::LevelDone { level } => {
357                    assert_eq!(level, 1);
358                    break;
359                }
360                other => panic!("unexpected {other:?}"),
361            }
362        }
363        // "-1_0" < "10_0" < "1_0": byte order of the keys, not numeric order.
364        assert_eq!(order, vec![(-1, 0), (10, 0), (1, 0)]);
365    }
366
367    #[test]
368    fn a_two_level_canvas_builds_each_level_from_the_one_below() {
369        // 256 × 64 at 64-pixel tiles: levels 1 and 2; four level-0 tiles in a row.
370        let (parents, done, queued) = run(
371            64,
372            256.0,
373            64.0,
374            &[(0, 0, 10), (1, 0, 20), (2, 0, 30), (3, 0, 40)],
375        );
376        assert_eq!(done, vec![1, 2]);
377        // Level 1: (0,0) from tiles 0,1 and (1,0) from tiles 2,3; level 2: (0,0) from both level-1 tiles.
378        assert_eq!(
379            parents.iter().map(|p| (p.0, p.1, p.2)).collect::<Vec<_>>(),
380            vec![(1, 0, 0), (1, 1, 0), (2, 0, 0)]
381        );
382        // The top-left quadrant of each parent is its first child halved: opaque, the child's value.
383        assert_eq!(parents[0].3, [10, 10, 10, 255]);
384        assert_eq!(parents[1].3, [30, 30, 30, 255]);
385        assert_eq!(parents[2].3, [10, 10, 10, 255]);
386        assert_eq!(queued, 4 + 2);
387    }
388
389    #[test]
390    fn a_missing_quadrant_stays_transparent() {
391        let (parents, ..) = run(64, 128.0, 128.0, &[(1, 1, 50)]);
392        assert_eq!(parents.len(), 1);
393        // Only the bottom-right child exists: the top-left pixel is untouched zero.
394        assert_eq!(parents[0].3, [0, 0, 0, 0]);
395    }
396}