use crate::engine::tiles::TileKey;
use crate::kernels::pyramid::assemble_parent;
use std::collections::BTreeMap;
pub fn level_count(width: f64, height: f64, size: usize) -> u32 {
let max_dimension = width.max(height);
let mut levels = 0;
let mut scale = size as f64;
while scale < max_dimension {
levels += 1;
scale *= 2.0;
}
levels
}
pub fn queue_key(x: i32, y: i32) -> String {
format!("{x}_{y}")
}
#[derive(Clone, Debug, PartialEq)]
pub enum Step {
NeedIndex {
level: u32,
},
NeedChildren {
level: u32,
x: i32,
y: i32,
children: [TileKey; 4],
},
Parent {
level: u32,
x: i32,
y: i32,
pixels: Option<Vec<u8>>,
},
LevelDone {
level: u32,
},
Done,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PyramidError {
BadSize,
Unexpected,
BadChild,
}
#[derive(Clone, Debug, PartialEq)]
enum Phase {
Start(u32),
Indexing(u32),
Parents(u32),
Children(u32, i32, i32),
Provided(u32, i32, i32),
Done,
}
pub struct PyramidSession {
canvas: String,
size: usize,
levels: u32,
phase: Phase,
queue: BTreeMap<String, (i32, i32)>,
children: [Option<Vec<u8>>; 4],
}
impl PyramidSession {
pub fn new(canvas: &str, width: f64, height: f64, size: usize) -> Result<Self, PyramidError> {
if size == 0 || !size.is_multiple_of(2) {
return Err(PyramidError::BadSize);
}
let levels = level_count(width, height, size);
Ok(Self {
canvas: canvas.to_string(),
size,
levels,
phase: if levels == 0 {
Phase::Done
} else {
Phase::Start(1)
},
queue: BTreeMap::new(),
children: [None, None, None, None],
})
}
pub fn levels(&self) -> u32 {
self.levels
}
pub fn queue(&mut self, x: i32, y: i32) -> Result<(i32, i32), PyramidError> {
if !matches!(self.phase, Phase::Indexing(_)) {
return Err(PyramidError::Unexpected);
}
let parent = (x.div_euclid(2), y.div_euclid(2));
self.queue.insert(queue_key(parent.0, parent.1), parent);
Ok(parent)
}
pub fn provide(&mut self, children: [Option<Vec<u8>>; 4]) -> Result<(), PyramidError> {
let Phase::Children(level, x, y) = self.phase else {
return Err(PyramidError::Unexpected);
};
let n = self.size * self.size * 4;
if children.iter().flatten().any(|c| c.len() != n) {
return Err(PyramidError::BadChild);
}
self.children = children;
self.phase = Phase::Provided(level, x, y);
Ok(())
}
pub fn step(&mut self) -> Step {
match self.phase.clone() {
Phase::Start(level) => {
self.phase = Phase::Indexing(level);
Step::NeedIndex { level }
}
Phase::Indexing(level) | Phase::Parents(level) => match self.queue.pop_first() {
Some((_, (x, y))) => {
self.phase = Phase::Children(level, x, y);
let child = |dx: i32, dy: i32| {
TileKey::new(&self.canvas, level - 1, x * 2 + dx, y * 2 + dy)
};
Step::NeedChildren {
level,
x,
y,
children: [child(0, 0), child(1, 0), child(0, 1), child(1, 1)],
}
}
None => {
self.phase = if level < self.levels {
Phase::Start(level + 1)
} else {
Phase::Done
};
Step::LevelDone { level }
}
},
Phase::Children(level, x, y) => {
let child = |dx: i32, dy: i32| {
TileKey::new(&self.canvas, level - 1, x * 2 + dx, y * 2 + dy)
};
Step::NeedChildren {
level,
x,
y,
children: [child(0, 0), child(1, 0), child(0, 1), child(1, 1)],
}
}
Phase::Provided(level, x, y) => {
self.phase = Phase::Parents(level);
let children = std::mem::take(&mut self.children);
let pixels = children.iter().any(Option::is_some).then(|| {
let mut out = vec![0u8; self.size * self.size * 4];
let [a, b, c, d] = &children;
assemble_parent(
[a.as_deref(), b.as_deref(), c.as_deref(), d.as_deref()],
self.size,
&mut out,
);
out
});
Step::Parent {
level,
x,
y,
pixels,
}
}
Phase::Done => Step::Done,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type Written = Vec<(u32, i32, i32, [u8; 4])>;
fn run(
size: usize,
width: f64,
height: f64,
level0: &[(i32, i32, u8)],
) -> (Written, Vec<u32>, usize) {
let mut session = PyramidSession::new("c", width, height, size).unwrap();
let mut store: BTreeMap<(u32, i32, i32), Vec<u8>> = BTreeMap::new();
for &(x, y, v) in level0 {
let mut px = vec![v; size * size * 4];
px.iter_mut().skip(3).step_by(4).for_each(|a| *a = 255);
store.insert((0, x, y), px);
}
let (mut parents, mut done, mut queued) = (Vec::new(), Vec::new(), 0);
loop {
match session.step() {
Step::NeedIndex { level } => {
let mut rows: Vec<(String, i32, i32)> = store
.keys()
.filter(|k| k.0 == level - 1)
.map(|&(_, x, y)| (queue_key(x, y), x, y))
.collect();
rows.sort();
for (_, x, y) in rows {
session.queue(x, y).unwrap();
queued += 1;
}
}
Step::NeedChildren { children, .. } => {
let got = children
.clone()
.map(|k| store.get(&(k.level, k.x, k.y)).cloned());
session.provide(got).unwrap();
}
Step::Parent {
level,
x,
y,
pixels,
} => {
if let Some(p) = pixels {
parents.push((level, x, y, [p[0], p[1], p[2], p[3]]));
store.insert((level, x, y), p);
}
}
Step::LevelDone { level } => done.push(level),
Step::Done => return (parents, done, queued),
}
}
}
#[test]
fn level_count_matches_the_doubling_table() {
let size = 64;
for (width, expected) in [
(1.0, 0),
(63.0, 0),
(64.0, 0),
(65.0, 1),
(128.0, 1),
(129.0, 2),
(256.0, 2),
(257.0, 3),
] {
assert_eq!(level_count(width, 1.0, size), expected, "width {width}");
assert_eq!(level_count(1.0, width, size), expected, "height {width}");
}
}
#[test]
fn a_canvas_within_one_tile_has_no_levels_and_asks_for_nothing() {
let mut s = PyramidSession::new("c", 10.0, 10.0, 64).unwrap();
assert_eq!(s.levels(), 0);
assert_eq!(s.step(), Step::Done);
}
#[test]
fn an_odd_or_zero_tile_size_is_refused() {
assert_eq!(
PyramidSession::new("c", 1.0, 1.0, 0).err(),
Some(PyramidError::BadSize)
);
assert_eq!(
PyramidSession::new("c", 1.0, 1.0, 15).err(),
Some(PyramidError::BadSize)
);
}
#[test]
fn queue_and_provide_out_of_turn_are_refused() {
let mut s = PyramidSession::new("c", 200.0, 10.0, 64).unwrap();
assert_eq!(s.queue(0, 0), Err(PyramidError::Unexpected));
assert_eq!(
s.provide([None, None, None, None]),
Err(PyramidError::Unexpected)
);
assert_eq!(s.step(), Step::NeedIndex { level: 1 });
assert_eq!(
s.provide([None, None, None, None]),
Err(PyramidError::Unexpected)
);
s.queue(0, 0).unwrap();
assert!(matches!(s.step(), Step::NeedChildren { .. }));
assert_eq!(s.queue(0, 0), Err(PyramidError::Unexpected));
assert_eq!(
s.provide([Some(vec![0; 3]), None, None, None]),
Err(PyramidError::BadChild)
);
}
#[test]
fn parents_floor_negative_coordinates_and_run_in_storage_key_order() {
let mut s = PyramidSession::new("c", 4096.0, 64.0, 64).unwrap();
assert_eq!(s.step(), Step::NeedIndex { level: 1 });
assert_eq!(s.queue(-1, 0).unwrap(), (-1, 0));
assert_eq!(s.queue(20, 0).unwrap(), (10, 0));
assert_eq!(s.queue(21, 1).unwrap(), (10, 0));
assert_eq!(s.queue(2, 0).unwrap(), (1, 0));
let mut order = Vec::new();
loop {
match s.step() {
Step::NeedChildren { x, y, children, .. } => {
order.push((x, y));
assert_eq!(children[1], TileKey::new("c", 0, x * 2 + 1, y * 2));
assert_eq!(children[2], TileKey::new("c", 0, x * 2, y * 2 + 1));
s.provide([None, None, None, None]).unwrap();
}
Step::Parent { pixels, .. } => assert!(pixels.is_none()),
Step::LevelDone { level } => {
assert_eq!(level, 1);
break;
}
other => panic!("unexpected {other:?}"),
}
}
assert_eq!(order, vec![(-1, 0), (10, 0), (1, 0)]);
}
#[test]
fn a_two_level_canvas_builds_each_level_from_the_one_below() {
let (parents, done, queued) = run(
64,
256.0,
64.0,
&[(0, 0, 10), (1, 0, 20), (2, 0, 30), (3, 0, 40)],
);
assert_eq!(done, vec![1, 2]);
assert_eq!(
parents.iter().map(|p| (p.0, p.1, p.2)).collect::<Vec<_>>(),
vec![(1, 0, 0), (1, 1, 0), (2, 0, 0)]
);
assert_eq!(parents[0].3, [10, 10, 10, 255]);
assert_eq!(parents[1].3, [30, 30, 30, 255]);
assert_eq!(parents[2].3, [10, 10, 10, 255]);
assert_eq!(queued, 4 + 2);
}
#[test]
fn a_missing_quadrant_stays_transparent() {
let (parents, ..) = run(64, 128.0, 128.0, &[(1, 1, 50)]);
assert_eq!(parents.len(), 1);
assert_eq!(parents[0].3, [0, 0, 0, 0]);
}
}