1use crate::engine::diagnostic::{Diagnostic, Severity};
21use crate::engine::tiles::{count_covered, Tile, TileCache, TileKey};
22use crate::kernels::framing::{
23 backgrounds, fold_evidence, frame_layout, paint_tile, Layout, OutputTile, SourceTile,
24};
25use crate::kernels::geometry::Rect;
26use crate::kernels::region::Region;
27use std::collections::HashSet;
28
29pub const DEFAULT_MAX_TILES: usize = 20000;
31
32#[derive(Clone, Debug, PartialEq)]
34pub struct SourceCanvas {
35 pub id: String,
36 pub bounds: Rect,
37 pub tile_count: u64,
38 pub attached: bool,
40}
41
42#[derive(Clone, Debug, PartialEq)]
44pub struct Reference {
45 pub frame: u32,
46 pub width: usize,
47 pub height: usize,
48 pub rgba: Vec<u8>,
49}
50
51#[derive(Clone, Debug, PartialEq)]
54pub struct FramedCanvas {
55 pub id: String,
57 pub width: f64,
58 pub height: f64,
59 pub source_region: Rect,
61 pub reference_frame: u32,
62 pub offset_x: f64,
64 pub offset_y: f64,
65 pub tile_count: u64,
67 pub observed_pixels: u64,
68 pub provisional_pixels: u64,
69}
70
71#[derive(Clone, Debug, PartialEq)]
72pub enum Outcome {
73 Unframed,
75 Skipped(Diagnostic),
78 Framed(FramedCanvas),
79}
80
81#[derive(Clone, Debug, PartialEq)]
83pub enum SaveTile {
84 Resident(u32),
86 Detached(Box<Tile>),
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum Step {
93 NeedReference,
95 NeedIndex,
97 Next,
99 NeedTile(TileKey),
101 Save(SaveTile),
103 Done(Outcome),
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum FramingError {
108 Unexpected,
110 BadReference,
112 NotProvided,
114}
115
116struct Work {
118 bounds: Rect,
119 output: Option<u32>,
120 output_copy: Option<Tile>,
121 sources: Vec<(i32, i32)>,
123 held: Vec<(i32, i32, u32, Option<Tile>)>,
125 pending: Option<TileKey>,
126}
127
128enum Phase {
129 Start,
130 AwaitReference,
132 Plan,
134 AwaitIndex,
136 Tiles,
137 Unframed,
139 Done,
140}
141
142#[derive(Default)]
144struct Planes {
145 pixels: Vec<u8>,
146 coverage: Vec<u8>,
147 provisional: Vec<u8>,
148 quality: Vec<u8>,
149 conflicts: Vec<u8>,
150 owner: Vec<u32>,
151 score: Vec<f32>,
152 frozen: Vec<u8>,
153}
154
155impl Planes {
156 fn swap(&mut self, t: &mut Tile) {
157 std::mem::swap(&mut self.pixels, &mut t.pixels);
158 std::mem::swap(&mut self.coverage, &mut t.coverage);
159 std::mem::swap(&mut self.provisional, &mut t.provisional);
160 std::mem::swap(&mut self.quality, &mut t.quality);
161 std::mem::swap(&mut self.conflicts, &mut t.conflicts);
162 std::mem::swap(&mut self.owner, &mut t.owner);
163 std::mem::swap(&mut self.score, &mut t.score);
164 std::mem::swap(&mut self.frozen, &mut t.frozen);
165 }
166
167 fn output(&mut self, size: usize) -> OutputTile<'_> {
168 OutputTile {
169 size,
170 pixels: &mut self.pixels,
171 coverage: &mut self.coverage,
172 provisional: &mut self.provisional,
173 quality: &mut self.quality,
174 score: &mut self.score,
175 owner: &mut self.owner,
176 conflicts: &mut self.conflicts,
177 frozen: &mut self.frozen,
178 }
179 }
180}
181
182pub struct FramingSession {
184 source: SourceCanvas,
185 pane: Rect,
186 ignore: Vec<Region>,
187 size: usize,
188 max_tiles: usize,
189 phase: Phase,
190 reference: Option<Reference>,
191 layout: Option<Layout>,
192 existing: Vec<(i32, i32)>,
193 existing_set: HashSet<(i32, i32)>,
194 candidates: Vec<(i64, i64)>,
195 candidate_set: HashSet<(i64, i64)>,
196 backgrounds: (Vec<u32>, Vec<u32>),
197 next: usize,
198 work: Option<Work>,
199 framed: Option<FramedCanvas>,
200}
201
202pub fn intersect(a: Rect, b: Rect) -> Rect {
204 let x = a.x.max(b.x);
205 let y = a.y.max(b.y);
206 Rect {
207 x,
208 y,
209 width: 0f64.max((a.x + a.width).min(b.x + b.width) - x),
210 height: 0f64.max((a.y + a.height).min(b.y + b.height) - y),
211 }
212}
213
214fn output_of(t: &mut Tile, size: usize) -> OutputTile<'_> {
215 OutputTile {
216 size,
217 pixels: &mut t.pixels,
218 coverage: &mut t.coverage,
219 provisional: &mut t.provisional,
220 quality: &mut t.quality,
221 score: &mut t.score,
222 owner: &mut t.owner,
223 conflicts: &mut t.conflicts,
224 frozen: &mut t.frozen,
225 }
226}
227
228fn source_of(t: &Tile, sx: i32, sy: i32) -> SourceTile<'_> {
229 SourceTile {
230 sx: sx as i64,
231 sy: sy as i64,
232 pixels: &t.pixels,
233 coverage: &t.coverage,
234 provisional: &t.provisional,
235 quality: &t.quality,
236 score: &t.score,
237 owner: &t.owner,
238 conflicts: &t.conflicts,
239 frozen: &t.frozen,
240 }
241}
242
243fn any_opaque(pixels: &[u8]) -> bool {
244 pixels.chunks_exact(4).any(|p| p[3] != 0)
245}
246
247impl FramingSession {
248 pub fn new(
251 source: SourceCanvas,
252 pane: Rect,
253 ignore: Vec<Region>,
254 size: usize,
255 max_tiles: usize,
256 ) -> Self {
257 Self {
258 source,
259 pane,
260 ignore,
261 size,
262 max_tiles,
263 phase: Phase::Start,
264 reference: None,
265 layout: None,
266 existing: Vec::new(),
267 existing_set: HashSet::new(),
268 candidates: Vec::new(),
269 candidate_set: HashSet::new(),
270 backgrounds: (Vec::new(), Vec::new()),
271 next: 0,
272 work: None,
273 framed: None,
274 }
275 }
276
277 pub fn framed_id(&self) -> String {
279 format!("{}-framed", self.source.id)
280 }
281
282 pub fn reference(&mut self, reference: Option<Reference>) -> Result<(), FramingError> {
284 if !matches!(self.phase, Phase::AwaitReference) {
285 return Err(FramingError::Unexpected);
286 }
287 if let Some(r) = &reference {
288 if r.width == 0 || r.height == 0 || r.rgba.len() != r.width * r.height * 4 {
289 return Err(FramingError::BadReference);
290 }
291 }
292 self.phase = if reference.is_some() {
293 Phase::Plan
294 } else {
295 Phase::Unframed
296 };
297 self.reference = reference;
298 Ok(())
299 }
300
301 pub fn index(&mut self, x: i32, y: i32, observed: u64) -> Result<(), FramingError> {
303 if !matches!(self.phase, Phase::AwaitIndex) {
304 return Err(FramingError::Unexpected);
305 }
306 if observed > 0 && self.existing_set.insert((x, y)) {
307 self.existing.push((x, y));
308 }
309 Ok(())
310 }
311
312 fn add_candidate(&mut self, tx: i64, ty: i64, cols: i64, rows: i64) -> bool {
314 if tx < 0 || tx >= cols || ty < 0 || ty >= rows || self.candidate_set.contains(&(tx, ty)) {
315 return true;
316 }
317 if self.candidates.len() >= self.max_tiles {
318 return false;
319 }
320 self.candidate_set.insert((tx, ty));
321 self.candidates.push((tx, ty));
322 true
323 }
324
325 fn add_range(&mut self, x0: i64, x1: i64, y0: i64, y1: i64, cols: i64, rows: i64) -> bool {
326 for ty in y0.max(0)..y1.min(rows) {
327 for tx in x0.max(0)..x1.min(cols) {
328 if !self.add_candidate(tx, ty, cols, rows) {
329 return false;
330 }
331 }
332 }
333 true
334 }
335
336 fn grid(&self) -> (i64, i64) {
337 let layout = self.layout.as_ref().unwrap();
338 let size = self.size as f64;
339 (
340 (layout.width / size).ceil() as i64,
341 (layout.height / size).ceil() as i64,
342 )
343 }
344
345 fn skipped(&mut self) -> Step {
346 self.phase = Phase::Done;
347 Step::Done(Outcome::Skipped(
348 Diagnostic::new("PRESENTATION_TOO_SPARSE", Severity::Warning)
349 .param("maxTiles", self.max_tiles as f64),
350 ))
351 }
352
353 fn plan_perimeter(&mut self) -> bool {
356 let layout = *self.layout.as_ref().unwrap();
357 let (cols, rows) = self.grid();
358 let size = self.size as f64;
359 let content = layout.content;
360 let bounded = |n: f64, upper: i64| (n as i64).clamp(0, upper.max(0));
361 let inner_x0 = bounded((content.x / size).ceil(), cols);
362 let inner_x1 = bounded(((content.x + content.width) / size).floor(), cols);
363 let inner_y0 = bounded((content.y / size).ceil(), rows);
364 let inner_y1 = bounded(((content.y + content.height) / size).floor(), rows);
365 self.add_range(0, cols, 0, inner_y0, cols, rows)
366 && self.add_range(0, cols, inner_y1, rows, cols, rows)
367 && self.add_range(0, inner_x0, inner_y0, inner_y1, cols, rows)
368 && self.add_range(inner_x1, cols, inner_y0, inner_y1, cols, rows)
369 }
370
371 fn plan_sources(&mut self) -> bool {
374 let layout = *self.layout.as_ref().unwrap();
375 let (cols, rows) = self.grid();
376 let size = self.size as f64;
377 let content = layout.content;
378 let bounds = self.source.bounds;
379 for i in 0..self.existing.len() {
380 let (sx, sy) = self.existing[i];
381 let mapped = Rect {
382 x: content.x + sx as f64 * size - bounds.x,
383 y: content.y + sy as f64 * size - bounds.y,
384 width: size,
385 height: size,
386 };
387 let overlap = intersect(mapped, content);
388 if overlap.width > 0.0 && overlap.height > 0.0 {
389 let x0 = (overlap.x / size).floor() as i64;
390 let x1 = ((overlap.x + overlap.width) / size).ceil() as i64;
391 let y0 = (overlap.y / size).floor() as i64;
392 let y1 = ((overlap.y + overlap.height) / size).ceil() as i64;
393 if !self.add_range(x0, x1, y0, y1, cols, rows) {
394 return false;
395 }
396 }
397 }
398 true
399 }
400
401 fn tile_bounds(&self, tx: i64, ty: i64) -> Rect {
402 let layout = self.layout.as_ref().unwrap();
403 let size = self.size as f64;
404 let (x, y) = (tx as f64 * size, ty as f64 * size);
405 Rect {
406 x,
407 y,
408 width: size.min(layout.width - x),
409 height: size.min(layout.height - y),
410 }
411 }
412
413 fn sources_under(&self, bounds: Rect) -> Vec<(i32, i32)> {
415 let layout = self.layout.as_ref().unwrap();
416 let overlap = intersect(bounds, layout.content);
417 let mut out = Vec::new();
418 if overlap.width > 0.0 && overlap.height > 0.0 {
419 let size = self.size as f64;
420 let wx = (self.source.bounds.x + overlap.x - layout.content.x).floor();
421 let wy = (self.source.bounds.y + overlap.y - layout.content.y).floor();
422 let (sy0, sy1) = (
423 (wy / size).floor() as i64,
424 ((wy + overlap.height - 1.0) / size).floor() as i64,
425 );
426 let (sx0, sx1) = (
427 (wx / size).floor() as i64,
428 ((wx + overlap.width - 1.0) / size).floor() as i64,
429 );
430 for sy in sy0..=sy1 {
431 for sx in sx0..=sx1 {
432 let key = (sx as i32, sy as i32);
433 if self.existing_set.contains(&key) {
434 out.push(key);
435 }
436 }
437 }
438 }
439 out
440 }
441
442 fn take(&mut self, cache: &mut TileCache, key: TileKey) -> Result<Option<u32>, FramingError> {
445 let work = self.work.as_mut().unwrap();
446 if let Some(pending) = work.pending.take() {
447 if pending != key {
448 return Err(FramingError::NotProvided);
449 }
450 return cache.id_of(&key).map(Some).ok_or(FramingError::NotProvided);
452 }
453 if let Some(id) = cache.touch(&key) {
454 return Ok(Some(id));
455 }
456 if let Some(id) = work.output {
457 if work.output_copy.is_none() {
458 work.output_copy = cache.get(id).cloned();
459 }
460 }
461 for (_, _, id, copy) in work.held.iter_mut() {
462 if copy.is_none() {
463 *copy = cache.get(*id).cloned();
464 }
465 }
466 work.pending = Some(key);
467 Ok(None)
468 }
469
470 pub fn step(&mut self, cache: &mut TileCache, now: f64) -> Result<Step, FramingError> {
471 match self.phase {
472 Phase::Start => {
473 if self.source.tile_count == 0 || self.source.attached {
474 self.phase = Phase::Done;
475 return Ok(Step::Done(Outcome::Unframed));
476 }
477 self.phase = Phase::AwaitReference;
478 Ok(Step::NeedReference)
479 }
480 Phase::Unframed => {
481 self.phase = Phase::Done;
482 Ok(Step::Done(Outcome::Unframed))
483 }
484 Phase::Plan => {
485 let reference = self.reference.as_ref().unwrap();
486 let (w, h) = (reference.width as f64, reference.height as f64);
487 let pane = self.pane;
488 if pane.x == 0.0 && pane.y == 0.0 && pane.width == w && pane.height == h {
489 self.phase = Phase::Done;
490 return Ok(Step::Done(Outcome::Unframed));
491 }
492 self.layout = Some(frame_layout(
493 w,
494 h,
495 pane,
496 self.source.bounds.width,
497 self.source.bounds.height,
498 ));
499 if !self.plan_perimeter() {
500 return Ok(self.skipped());
501 }
502 self.phase = Phase::AwaitIndex;
503 Ok(Step::NeedIndex)
504 }
505 Phase::AwaitIndex => {
506 if !self.plan_sources() {
507 return Ok(self.skipped());
508 }
509 let layout = *self.layout.as_ref().unwrap();
510 let r = self.reference.as_ref().unwrap();
511 self.backgrounds = backgrounds(&r.rgba, r.width, r.height, layout.pane);
512 self.framed = Some(FramedCanvas {
513 id: self.framed_id(),
514 width: layout.width,
515 height: layout.height,
516 source_region: layout.pane,
517 reference_frame: r.frame,
518 offset_x: layout.content.x - self.source.bounds.x,
519 offset_y: layout.content.y - self.source.bounds.y,
520 tile_count: 0,
521 observed_pixels: 0,
522 provisional_pixels: 0,
523 });
524 self.phase = Phase::Tiles;
525 Ok(self.next_tile())
526 }
527 Phase::Tiles => self.tiles(cache, now),
528 Phase::AwaitReference | Phase::Done => Err(FramingError::Unexpected),
529 }
530 }
531
532 fn next_tile(&mut self) -> Step {
534 let Some(&(tx, ty)) = self.candidates.get(self.next) else {
535 self.phase = Phase::Done;
536 self.work = None;
537 return Step::Done(Outcome::Framed(self.framed.take().unwrap()));
538 };
539 self.next += 1;
540 let bounds = self.tile_bounds(tx, ty);
541 self.work = Some(Work {
542 bounds,
543 output: None,
544 output_copy: None,
545 sources: self.sources_under(bounds),
546 held: Vec::new(),
547 pending: None,
548 });
549 Step::Next
550 }
551
552 fn tiles(&mut self, cache: &mut TileCache, now: f64) -> Result<Step, FramingError> {
553 if self.work.is_none() {
554 return Ok(self.next_tile());
555 }
556 let size = self.size;
557 if self.work.as_ref().unwrap().output.is_none() {
558 let (tx, ty) = self.candidates[self.next - 1];
559 let key = TileKey::new(&self.framed_id(), 0, tx as i32, ty as i32);
560 let Some(id) = self.take(cache, key.clone())? else {
561 return Ok(Step::NeedTile(key));
562 };
563 let work = self.work.as_mut().unwrap();
564 work.output = Some(id);
565 let (layout, r) = (
566 self.layout.as_ref().unwrap(),
567 self.reference.as_ref().unwrap(),
568 );
569 paint_tile(
570 &mut output_of(cache.get_mut(id).unwrap(), size),
571 layout,
572 work.bounds,
573 &r.rgba,
574 r.width,
575 r.height,
576 &self.backgrounds.0,
577 &self.backgrounds.1,
578 &self.ignore,
579 );
580 }
581 loop {
582 let work = self.work.as_ref().unwrap();
583 let Some(&(sx, sy)) = work.sources.get(work.held.len()) else {
584 break;
585 };
586 let key = TileKey::new(&self.source.id, 0, sx, sy);
587 let Some(id) = self.take(cache, key.clone())? else {
588 return Ok(Step::NeedTile(key));
589 };
590 self.work.as_mut().unwrap().held.push((sx, sy, id, None));
591 }
592 self.finish_tile(cache, now)
593 }
594
595 fn finish_tile(&mut self, cache: &mut TileCache, now: f64) -> Result<Step, FramingError> {
598 let size = self.size;
599 let layout = *self.layout.as_ref().unwrap();
600 let (bx, by) = (self.source.bounds.x, self.source.bounds.y);
601 let mut work = self.work.take().unwrap();
602 let output = work.output.unwrap();
603 let mut detached = match cache.get(output) {
605 Some(_) => None,
606 None => Some(work.output_copy.take().ok_or(FramingError::NotProvided)?),
607 };
608 if !work.held.is_empty() {
609 let mut planes = Planes::default();
610 if detached.is_none() {
611 planes.swap(cache.get_mut(output).unwrap());
613 }
614 {
615 let mut sources = Vec::with_capacity(work.held.len());
616 for (sx, sy, id, copy) in &work.held {
617 let t = match cache.get(*id) {
618 Some(t) => t,
619 None => copy.as_ref().ok_or(FramingError::NotProvided)?,
620 };
621 sources.push(source_of(t, *sx, *sy));
622 }
623 let mut out = match detached.as_mut() {
624 Some(t) => output_of(t, size),
625 None => planes.output(size),
626 };
627 fold_evidence(&mut out, &layout, bx, by, work.bounds, &sources);
628 }
629 if detached.is_none() {
630 planes.swap(cache.get_mut(output).unwrap());
631 }
632 }
633 let tile = match detached.as_mut() {
634 Some(t) => t,
635 None => cache.get_mut(output).unwrap(),
636 };
637 if !any_opaque(&tile.pixels) {
638 return Ok(self.next_tile());
639 }
640 tile.dirty = true;
641 tile.touched = Some(now);
642 let framed = self.framed.as_mut().unwrap();
643 framed.tile_count += 1;
644 framed.observed_pixels += count_covered(&tile.coverage);
645 framed.provisional_pixels += count_covered(&tile.provisional);
646 Ok(Step::Save(match detached {
647 Some(t) => SaveTile::Detached(Box::new(t)),
648 None => SaveTile::Resident(output),
649 }))
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::engine::tiles::StoredTile;
657 use std::collections::HashMap;
658
659 const S: usize = 16;
660
661 fn rect(x: f64, y: f64, width: f64, height: f64) -> Rect {
662 Rect {
663 x,
664 y,
665 width,
666 height,
667 }
668 }
669
670 fn reference(alpha: u8) -> Reference {
672 let (width, height) = (40, 30);
673 let mut rgba = Vec::with_capacity(width * height * 4);
674 for y in 0..height {
675 for x in 0..width {
676 rgba.extend_from_slice(&[(x * 5) as u8, (y * 5) as u8, 77, alpha]);
677 }
678 }
679 Reference {
680 frame: 3,
681 width,
682 height,
683 rgba,
684 }
685 }
686
687 fn source() -> SourceCanvas {
690 SourceCanvas {
691 id: "src".into(),
692 bounds: rect(0.0, 0.0, 60.0, 50.0),
693 tile_count: 12,
694 attached: false,
695 }
696 }
697
698 fn session(source: SourceCanvas, pane: Rect, max_tiles: usize) -> FramingSession {
699 FramingSession::new(source, pane, Vec::new(), S, max_tiles)
700 }
701
702 const PANE: Rect = Rect {
703 x: 4.0,
704 y: 4.0,
705 width: 24.0,
706 height: 20.0,
707 };
708
709 fn index() -> Vec<(i32, i32, u64)> {
711 let mut rows: Vec<(String, i32, i32)> = (0..4)
712 .flat_map(|x| (0..4).map(move |y| (format!("{x}_{y}"), x, y)))
713 .collect();
714 rows.sort();
715 rows.into_iter().map(|(_, x, y)| (x, y, 256)).collect()
716 }
717
718 #[derive(Default)]
719 struct Host {
720 store: HashMap<TileKey, StoredTile>,
721 needs: Vec<TileKey>,
722 saves: Vec<TileKey>,
723 detached: usize,
724 nexts: usize,
725 reference_asked: bool,
726 index_asked: bool,
727 }
728
729 impl Host {
730 fn with_sources() -> Host {
731 let mut host = Host::default();
732 for (x, y, _) in index() {
733 let n = S * S;
734 let mut pixels = vec![0u8; n * 4];
735 for p in pixels.chunks_exact_mut(4) {
736 p.copy_from_slice(&[x as u8 * 40, y as u8 * 40, 200, 255]);
737 }
738 let mut provisional = vec![0u8; n / 8];
739 provisional[0] = 1;
740 host.store.insert(
741 TileKey::new("src", 0, x, y),
742 StoredTile {
743 pixels,
744 coverage: Some(vec![255; n / 8]),
745 provisional: Some(provisional),
746 quality: Some(vec![(x * 10 + y) as u8; 1]),
747 conflicts: Some(vec![(x == 1) as u8; 1]),
748 owner: Some(vec![(y + 1) as u32; 1]),
749 score: Some(vec![x as f32; 1]),
750 frozen: Some(vec![(y == 2) as u8; 1]),
751 },
752 );
753 }
754 host
755 }
756
757 fn keep(&mut self, t: Tile) {
758 self.store.insert(
759 t.key.clone(),
760 StoredTile {
761 pixels: t.pixels,
762 coverage: Some(t.coverage),
763 provisional: Some(t.provisional),
764 quality: Some(t.quality),
765 conflicts: Some(t.conflicts),
766 owner: Some(t.owner),
767 score: Some(t.score),
768 frozen: Some(t.frozen),
769 },
770 );
771 }
772
773 fn run(
774 &mut self,
775 s: &mut FramingSession,
776 cache: &mut TileCache,
777 reference: Option<Reference>,
778 index: &[(i32, i32, u64)],
779 ) -> Outcome {
780 loop {
781 match s.step(cache, 1.0).unwrap() {
782 Step::NeedReference => {
783 self.reference_asked = true;
784 s.reference(reference.clone()).unwrap();
785 }
786 Step::NeedIndex => {
787 self.index_asked = true;
788 for &(x, y, observed) in index {
789 s.index(x, y, observed).unwrap();
790 }
791 }
792 Step::Next => self.nexts += 1,
793 Step::NeedTile(key) => {
794 self.needs.push(key.clone());
795 while let Some(id) = cache.victim() {
796 let t = cache.evict(id).unwrap();
797 if t.dirty {
798 self.keep(t);
799 }
800 }
801 let stored = self.store.get(&key).cloned();
802 cache.insert(key, stored).unwrap();
803 }
804 Step::Save(SaveTile::Resident(id)) => {
805 let t = cache.get(id).unwrap().clone();
806 assert!(t.dirty);
807 self.saves.push(t.key.clone());
808 self.keep(t);
809 cache.encoded();
810 cache.saved(id);
811 }
812 Step::Save(SaveTile::Detached(t)) => {
813 assert!(t.dirty);
814 self.detached += 1;
815 self.saves.push(t.key.clone());
816 self.keep(*t);
817 cache.encoded();
818 }
819 Step::Done(outcome) => return outcome,
820 }
821 }
822 }
823 }
824
825 fn framed(x: i32, y: i32) -> TileKey {
826 TileKey::new("src-framed", 0, x, y)
827 }
828
829 fn src(x: i32, y: i32) -> TileKey {
830 TileKey::new("src", 0, x, y)
831 }
832
833 #[test]
834 fn an_empty_or_attached_canvas_is_not_framed_and_reads_nothing() {
835 for canvas in [
836 SourceCanvas {
837 tile_count: 0,
838 ..source()
839 },
840 SourceCanvas {
841 attached: true,
842 ..source()
843 },
844 ] {
845 let mut cache = TileCache::new(S, 128.0).unwrap();
846 let mut host = Host::default();
847 let outcome = host.run(
848 &mut session(canvas, PANE, DEFAULT_MAX_TILES),
849 &mut cache,
850 Some(reference(255)),
851 &index(),
852 );
853 assert_eq!(outcome, Outcome::Unframed);
854 assert!(!host.reference_asked && !host.index_asked && host.needs.is_empty());
855 }
856 }
857
858 #[test]
859 fn without_a_reference_frame_nothing_is_framed() {
860 let mut cache = TileCache::new(S, 128.0).unwrap();
861 let mut host = Host::default();
862 let outcome = host.run(
863 &mut session(source(), PANE, DEFAULT_MAX_TILES),
864 &mut cache,
865 None,
866 &index(),
867 );
868 assert_eq!(outcome, Outcome::Unframed);
869 assert!(host.reference_asked && !host.index_asked && host.needs.is_empty());
870 }
871
872 #[test]
873 fn a_pane_that_is_the_whole_frame_is_not_framed() {
874 let mut cache = TileCache::new(S, 128.0).unwrap();
875 let mut host = Host::default();
876 let outcome = host.run(
877 &mut session(source(), rect(0.0, 0.0, 40.0, 30.0), DEFAULT_MAX_TILES),
878 &mut cache,
879 Some(reference(255)),
880 &index(),
881 );
882 assert_eq!(outcome, Outcome::Unframed);
883 assert!(!host.index_asked && host.needs.is_empty());
884 }
885
886 #[test]
887 fn a_perimeter_over_the_limit_is_skipped_before_the_index_is_read() {
888 let mut cache = TileCache::new(S, 128.0).unwrap();
889 let mut host = Host::with_sources();
890 let outcome = host.run(
891 &mut session(source(), PANE, 13),
892 &mut cache,
893 Some(reference(255)),
894 &index(),
895 );
896 let Outcome::Skipped(d) = outcome else {
897 panic!("expected a skip, got {outcome:?}");
898 };
899 assert_eq!(d.code, "PRESENTATION_TOO_SPARSE");
900 assert_eq!(d.severity, Severity::Warning);
901 assert_eq!(d.params, vec![("maxTiles", 13.0)]);
902 assert!(!host.index_asked && host.needs.is_empty() && cache.is_empty());
903 }
904
905 #[test]
906 fn source_tiles_over_the_limit_skip_the_canvas_before_any_tile_is_touched() {
907 let mut cache = TileCache::new(S, 128.0).unwrap();
908 let mut host = Host::with_sources();
909 let outcome = host.run(
911 &mut session(source(), PANE, 14),
912 &mut cache,
913 Some(reference(255)),
914 &index(),
915 );
916 assert!(matches!(outcome, Outcome::Skipped(_)), "{outcome:?}");
917 assert!(host.index_asked && host.needs.is_empty() && host.nexts == 0 && cache.is_empty());
918 }
919
920 #[test]
921 fn tiles_are_planned_perimeter_first_then_under_each_observed_source_tile_in_index_order() {
922 let mut cache = TileCache::new(S, 128.0).unwrap();
923 let mut host = Host::with_sources();
924 host.run(
925 &mut session(source(), PANE, DEFAULT_MAX_TILES),
926 &mut cache,
927 Some(reference(255)),
928 &index(),
929 );
930 let outputs: Vec<TileKey> = host
931 .needs
932 .iter()
933 .filter(|k| k.canvas == "src-framed")
934 .cloned()
935 .collect();
936 let expected: Vec<TileKey> = [
937 (0, 0),
939 (1, 0),
940 (2, 0),
941 (3, 0),
942 (4, 0),
943 (0, 3),
944 (1, 3),
945 (2, 3),
946 (3, 3),
947 (4, 3),
948 (0, 1),
949 (0, 2),
950 (4, 1),
951 (4, 2),
952 (1, 1),
954 (1, 2),
955 (2, 1),
956 (2, 2),
957 (3, 1),
958 (3, 2),
959 ]
960 .iter()
961 .map(|&(x, y)| framed(x, y))
962 .collect();
963 assert_eq!(outputs, expected);
964 assert_eq!(host.nexts, 20);
965 }
966
967 #[test]
968 fn unobserved_source_tiles_plan_nothing() {
969 let mut cache = TileCache::new(S, 128.0).unwrap();
970 let mut host = Host::with_sources();
971 let unobserved: Vec<(i32, i32, u64)> =
972 index().into_iter().map(|(x, y, _)| (x, y, 0)).collect();
973 let outcome = host.run(
974 &mut session(source(), PANE, DEFAULT_MAX_TILES),
975 &mut cache,
976 Some(reference(255)),
977 &unobserved,
978 );
979 assert_eq!(host.nexts, 14);
980 assert!(host.needs.iter().all(|k| k.canvas == "src-framed"));
981 let Outcome::Framed(f) = outcome else {
982 panic!("expected a framed canvas");
983 };
984 assert_eq!(f.observed_pixels, host_coverage(&host));
985 }
986
987 fn host_coverage(host: &Host) -> u64 {
988 host.saves
989 .iter()
990 .map(|k| count_covered(host.store[k].coverage.as_ref().unwrap()))
991 .sum()
992 }
993
994 #[test]
995 fn each_output_tile_takes_itself_then_its_source_tiles_row_by_row() {
996 let mut cache = TileCache::new(S, 128.0).unwrap();
997 let mut host = Host::with_sources();
998 host.run(
999 &mut session(source(), PANE, DEFAULT_MAX_TILES),
1000 &mut cache,
1001 Some(reference(255)),
1002 &index(),
1003 );
1004 assert_eq!(
1006 host.needs[..6],
1007 [
1008 framed(0, 0),
1009 src(0, 0),
1010 framed(1, 0),
1011 src(1, 0),
1012 framed(2, 0),
1013 src(2, 0)
1014 ]
1015 );
1016 }
1017
1018 #[test]
1019 fn a_cache_of_two_tiles_frames_exactly_what_a_large_one_does() {
1020 let run = |max: u64| {
1021 let mut cache = TileCache::new(S, 128.0).unwrap();
1022 cache.set_max_tiles(max);
1023 let mut host = Host::with_sources();
1024 let outcome = host.run(
1025 &mut session(source(), PANE, DEFAULT_MAX_TILES),
1026 &mut cache,
1027 Some(reference(255)),
1028 &index(),
1029 );
1030 (outcome, host)
1031 };
1032 let (large, large_host) = run(1000);
1033 let (small, small_host) = run(2);
1034 assert_eq!(large, small);
1035 assert_eq!(large_host.detached, 0);
1036 assert!(
1037 small_host.detached > 0,
1038 "the small cache never evicted an output tile"
1039 );
1040 assert_eq!(large_host.saves, small_host.saves);
1041 for key in &large_host.saves {
1042 assert_eq!(large_host.store[key], small_host.store[key], "{key:?}");
1043 }
1044 let Outcome::Framed(f) = large else {
1045 panic!("expected a framed canvas");
1046 };
1047 assert_eq!(f.id, "src-framed");
1048 assert_eq!((f.width, f.height), (76.0, 60.0));
1049 assert_eq!(f.source_region, PANE);
1050 assert_eq!((f.offset_x, f.offset_y, f.reference_frame), (4.0, 4.0, 3));
1051 assert_eq!(f.tile_count, 20);
1052 assert_eq!(f.observed_pixels, host_coverage(&large_host));
1053 assert_eq!(f.provisional_pixels, 16);
1055 }
1056
1057 #[test]
1058 fn an_output_tile_that_stays_transparent_is_not_saved() {
1059 let mut cache = TileCache::new(S, 128.0).unwrap();
1060 let mut host = Host::default();
1061 let outcome = host.run(
1062 &mut session(source(), PANE, DEFAULT_MAX_TILES),
1063 &mut cache,
1064 Some(reference(0)),
1065 &[],
1066 );
1067 let Outcome::Framed(f) = outcome else {
1068 panic!("expected a framed canvas");
1069 };
1070 assert_eq!((f.tile_count, f.observed_pixels), (0, 0));
1071 assert_eq!(host.nexts, 14);
1072 assert!(host.saves.is_empty());
1073 assert!(cache.dirty(None).is_empty());
1074 }
1075
1076 #[test]
1077 fn calls_out_of_turn_and_a_tile_not_brought_in_are_refused() {
1078 let mut cache = TileCache::new(S, 128.0).unwrap();
1079 let mut s = session(source(), PANE, DEFAULT_MAX_TILES);
1080 assert_eq!(s.reference(None), Err(FramingError::Unexpected));
1081 assert_eq!(s.index(0, 0, 1), Err(FramingError::Unexpected));
1082 assert_eq!(s.step(&mut cache, 0.0), Ok(Step::NeedReference));
1083 assert_eq!(s.step(&mut cache, 0.0), Err(FramingError::Unexpected));
1084 let mut bad = reference(255);
1085 bad.rgba.pop();
1086 assert_eq!(s.reference(Some(bad)), Err(FramingError::BadReference));
1087 s.reference(Some(reference(255))).unwrap();
1088 assert_eq!(s.step(&mut cache, 0.0), Ok(Step::NeedIndex));
1089 assert_eq!(s.step(&mut cache, 0.0), Ok(Step::Next));
1090 assert_eq!(s.step(&mut cache, 0.0), Ok(Step::NeedTile(framed(0, 0))));
1091 assert_eq!(s.step(&mut cache, 0.0), Err(FramingError::NotProvided));
1092 }
1093}