use crate::engine::reveal::{
MemoryStage, ObservedDecision, Outcome, Pass, Provenance, RevealCanvas, RevealLayer,
RevealSession, Step,
};
use crate::engine::tiles::{StoredTile, Tile, TileCache, TileKey};
use crate::kernels::geometry::Rect;
use std::collections::BTreeMap;
const SIZE: usize = 32;
const W: usize = 160;
const H: usize = 160;
const PAGE_H: usize = 480;
const STEP: usize = 16;
const BUTTON: (usize, usize) = (64, 96);
const MAGENTA: [u8; 4] = [255, 0, 255, 255];
fn page(x: usize, y: usize) -> [u8; 4] {
let h = ((x / 4) as u32)
.wrapping_mul(2_654_435_761)
.wrapping_add(((y / 4) as u32).wrapping_mul(40_503))
.rotate_left(13)
.wrapping_mul(2_246_822_519);
[(h >> 24) as u8, (h >> 16) as u8, (h >> 8) as u8, 255]
}
fn frame(k: usize) -> Vec<u8> {
let mut rgba = Vec::with_capacity(W * H * 4);
for sy in 0..H {
for sx in 0..W {
let button =
(BUTTON.0..BUTTON.0 + 16).contains(&sx) && (BUTTON.1..BUTTON.1 + 16).contains(&sy);
rgba.extend_from_slice(&if button {
MAGENTA
} else {
page(sx, sy + STEP * k)
});
}
}
rgba
}
#[derive(Default)]
struct Store {
tiles: BTreeMap<String, StoredTile>,
}
impl Store {
fn put(&mut self, t: &Tile) {
self.tiles.insert(
t.key.path(),
StoredTile {
pixels: t.pixels.clone(),
coverage: Some(t.coverage.clone()),
provisional: Some(t.provisional.clone()),
quality: Some(t.quality.clone()),
conflicts: Some(t.conflicts.clone()),
owner: Some(t.owner.clone()),
score: Some(t.score.clone()),
frozen: Some(t.frozen.clone()),
},
);
}
fn pixel(&self, x: usize, y: usize) -> [u8; 4] {
let key = TileKey::new("world", 0, (x / SIZE) as i32, (y / SIZE) as i32);
let t = &self.tiles[&key.path()];
let p = ((y % SIZE) * SIZE + x % SIZE) * 4;
t.pixels[p..p + 4].try_into().unwrap()
}
}
fn load(cache: &mut TileCache, store: &mut Store, key: TileKey, loads: &mut usize) {
while let Some(id) = cache.victim() {
let t = cache.evict(id).unwrap();
if t.dirty {
cache.encoded();
store.put(&t);
}
}
let stored = store.tiles.get(&key.path()).cloned();
cache.insert(key, stored).unwrap();
*loads += 1;
}
#[test]
fn a_native_host_reveals_the_page_under_a_floating_button() {
let frames: Vec<Vec<u8>> = (0..=(PAGE_H - H) / STEP).map(frame).collect();
let mut store = Store::default();
for ty in 0..PAGE_H / SIZE {
for tx in 0..W / SIZE {
let mut pixels = vec![0u8; SIZE * SIZE * 4];
for ly in 0..SIZE {
for lx in 0..SIZE {
let (x, y) = (tx * SIZE + lx, ty * SIZE + ly);
let k = (y + 1).saturating_sub(H).div_ceil(STEP);
let p = ((y - STEP * k) * W + x) * 4;
pixels[(ly * SIZE + lx) * 4..(ly * SIZE + lx) * 4 + 4]
.copy_from_slice(&frames[k][p..p + 4]);
}
}
store.tiles.insert(
TileKey::new("world", 0, tx as i32, ty as i32).path(),
StoredTile {
pixels,
coverage: Some(vec![0xff; SIZE * SIZE / 8]),
..StoredTile::default()
},
);
}
}
assert_eq!(store.pixel(70, 100), MAGENTA, "the button is burned in");
let full = |w: usize, h: usize| Rect {
x: 0.0,
y: 0.0,
width: w as f64,
height: h as f64,
};
let mut session = RevealSession::new(
W,
H,
10.0,
SIZE,
frames.len() as u32,
vec![RevealLayer {
id: "page".into(),
rect: full(W, H),
code: 0,
}],
vec![RevealCanvas {
id: "world".into(),
bounds: full(W, PAGE_H),
}],
)
.unwrap();
let mut cache = TileCache::new(SIZE, 128.0).unwrap();
cache.set_max_tiles(4);
let (mut loads, mut checkpoints) = (0, 0);
let (mut passes, mut memory) = (Vec::new(), Vec::new());
let mut provenance: Vec<Provenance> = Vec::new();
let mut now = 0.0;
let summary = loop {
now += 1.0;
match session.step(&mut cache, now).unwrap() {
Step::NeedMemory { bytes, stage } => {
memory.push((stage, bytes));
session.memory(true, 0.0).unwrap();
}
Step::NeedObservations => {
for k in 0..frames.len() {
let d = ObservedDecision {
canvas: "world".into(),
layer: "page".into(),
x: 0.0,
y: (STEP * k) as f64,
skip: false,
skipped: false,
occlusions: Vec::new(),
};
session.observation(k as u32, &[d]).unwrap();
}
}
Step::NeedTile(key) => load(&mut cache, &mut store, key, &mut loads),
Step::Checkpoint => checkpoints += 1,
Step::Decode { pass, wanted } => {
passes.push(pass);
assert_eq!(wanted.len(), frames.len());
for (k, f) in frames.iter().enumerate() {
if wanted[k] {
session.frame_buffer().unwrap().copy_from_slice(f);
}
session.push_frame(k as u32, None).unwrap();
loop {
match session.step(&mut cache, now).unwrap() {
Step::NextFrame => break,
Step::NeedTile(key) => load(&mut cache, &mut store, key, &mut loads),
other => panic!("unexpected {other:?} inside a frame"),
}
}
}
session.end_pass(None).unwrap();
loop {
match session.step(&mut cache, now).unwrap() {
Step::PassDone(p) => {
assert_eq!(p, pass);
break;
}
Step::NeedTile(key) => load(&mut cache, &mut store, key, &mut loads),
other => panic!("unexpected {other:?} at the end of a pass"),
}
}
}
Step::Provenance(p) => provenance.push(p),
Step::Done(Outcome::Revealed(summary)) => break summary,
other => panic!("unexpected {other:?}"),
}
};
for id in cache.dirty(None) {
store.put(cache.get(id).unwrap());
cache.saved(id);
}
assert_eq!(memory[0].0, MemoryStage::Canvases);
assert!(memory[0].1 > (3 * W * H * 4) as f64);
assert_eq!(memory[1], (MemoryStage::Fallback, 0.0));
assert_eq!(
passes,
vec![Pass::Learn, Pass::Agree, Pass::Relearn, Pass::Write]
);
assert_eq!(
checkpoints,
(W / SIZE) * (PAGE_H / SIZE),
"one checkpoint per compositor tile"
);
assert!(loads > checkpoints, "tiles came back after eviction");
assert!(summary.objects >= 1, "the button was modelled: {summary:?}");
for y in 0..PAGE_H {
for x in 0..W {
assert_eq!(store.pixel(x, y), page(x, y), "world ({x}, {y})");
}
}
let [p] = &provenance[..] else {
panic!("one provenance row, got {}", provenance.len());
};
assert_eq!(
(p.canvas.as_str(), p.gx, p.gy, p.bw),
("world", 0, 0, W / 16)
);
let source = p.source[(BUTTON.1 / 16) * p.bw + BUTTON.0 / 16];
assert!(
source >= 1,
"block under the button comes from frame {source}"
);
}