Skip to main content

long_screen/engine/
framing.rs

1//! The framed presentation canvas of one reconstructed canvas (docs/ARCHITECTURE.md §十 "呈现画布"): the reference
2//! frame's native chrome around the reconstructed content, with flat background bands where the content is taller or
3//! wider than the pane it was seen through. No toolbar is translated into world coordinates, no sidebar icon is
4//! stretched or repeated, and background extensions never count as observed evidence.
5//!
6//! [`FramingSession`] builds one framed canvas as a resumable job over the tile cache. Its cost is
7//! O(perimeter tiles plus observed source tiles), never O(bounding-box area / tile²): before touching an output tile it plans, with grid
8//! arithmetic only, the tiles that can be non-empty — those that reach outside the content rect (chrome and
9//! background bands, kept so the frame stays continuous) and those under at least one observed source tile — and
10//! refuses a plan of more than `max_tiles` tiles before any tile work starts.
11//!
12//! The host answers what each [`FramingSession::step`] asks for: the reference frame ([`Step::NeedReference`]), the
13//! source canvas's level-0 tile index ([`Step::NeedIndex`], asked only once the perimeter alone fits the limit), and
14//! tiles that are not resident ([`Step::NeedTile`]: make room with [`TileCache::victim`], storing what is dirty
15//! before [`TileCache::evict`], then [`TileCache::insert`]). [`Step::Next`] starts each output tile and is where the host checks for pause and stop;
16//! [`Step::Save`] hands back an output tile that received pixels, for the host to store now. Per output tile the
17//! session takes the output tile first, then the source tiles under it, row by row, each touched in the cache or
18//! asked for, so the cache sees the same sequence of uses as it always has.
19
20use 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
29/// Planned output tiles a framed canvas may have before it is skipped.
30pub const DEFAULT_MAX_TILES: usize = 20000;
31
32/// What the session needs to know about the canvas it frames.
33#[derive(Clone, Debug, PartialEq)]
34pub struct SourceCanvas {
35    pub id: String,
36    pub bounds: Rect,
37    pub tile_count: u64,
38    /// Attached to another canvas (a fragment shown inside it): not framed on its own.
39    pub attached: bool,
40}
41
42/// The reference frame the chrome is copied from, decoded by the host.
43#[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/// A framed canvas the session built: its id and size, where the content sits, and the counters it recomputed from
52/// the pixels it copied.
53#[derive(Clone, Debug, PartialEq)]
54pub struct FramedCanvas {
55    /// `<source id>-framed`.
56    pub id: String,
57    pub width: f64,
58    pub height: f64,
59    /// The pane of the reference frame the content was seen through.
60    pub source_region: Rect,
61    pub reference_frame: u32,
62    /// Where the source canvas's origin lands on the framed canvas.
63    pub offset_x: f64,
64    pub offset_y: f64,
65    /// Output tiles that received pixels (and were saved).
66    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    /// Nothing to frame: no reference frame, an empty or attached canvas, or a pane that is the whole frame.
74    Unframed,
75    /// `PRESENTATION_TOO_SPARSE` (warning, parameter `maxTiles`): the plan exceeded the tile limit; no tile was
76    /// touched.
77    Skipped(Diagnostic),
78    Framed(FramedCanvas),
79}
80
81/// An output tile to store now.
82#[derive(Clone, Debug, PartialEq)]
83pub enum SaveTile {
84    /// Still resident in the cache, by id (the host stores it and reports [`TileCache::saved`]).
85    Resident(u32),
86    /// The cache evicted it while its source tiles were being brought in; the session kept its planes, and the host
87    /// stores this copy.
88    Detached(Box<Tile>),
89}
90
91#[derive(Clone, Debug, PartialEq)]
92pub enum Step {
93    /// Read the reference frame and pass it to [`FramingSession::reference`], then step again.
94    NeedReference,
95    /// Pass every stored level-0 tile-index row of the source canvas to [`FramingSession::index`], then step again.
96    NeedIndex,
97    /// The next output tile starts; check for pause and stop, then step again.
98    Next,
99    /// The tile is not resident: make room, insert it, step again.
100    NeedTile(TileKey),
101    /// Store this output tile now, then step again.
102    Save(SaveTile),
103    Done(Outcome),
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum FramingError {
108    /// `reference` or `index` out of turn, or a step after the session finished.
109    Unexpected,
110    /// The reference frame's pixels do not hold `width × height` RGBA pixels.
111    BadReference,
112    /// Stepped again after [`Step::NeedTile`] without inserting the tile.
113    NotProvided,
114}
115
116/// Where one tile of the plan is in its work.
117struct Work {
118    bounds: Rect,
119    output: Option<u32>,
120    output_copy: Option<Tile>,
121    /// The existing source tiles under this output tile, in the order they are taken.
122    sources: Vec<(i32, i32)>,
123    /// The source tiles taken so far: cache id and, once the cache could have evicted it, a copy.
124    held: Vec<(i32, i32, u32, Option<Tile>)>,
125    pending: Option<TileKey>,
126}
127
128enum Phase {
129    Start,
130    /// [`Step::NeedReference`] was returned.
131    AwaitReference,
132    /// The reference frame is in: plan the perimeter.
133    Plan,
134    /// [`Step::NeedIndex`] was returned.
135    AwaitIndex,
136    Tiles,
137    /// No reference frame: the next step reports [`Outcome::Unframed`].
138    Unframed,
139    Done,
140}
141
142/// An output tile's planes while they are out of the cache, so its source tiles can be read from the cache.
143#[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
182/// One framed canvas in progress.
183pub 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
202/// Intersection of the pane with its crop, or of an output tile with the content.
203pub 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    /// A session framing `source`, seen through `pane` of the reference frame (the region rectangle intersected with its crop), with
249    /// `ignore` regions left out of the chrome, over the cache's `size`-pixel tiles.
250    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    /// The id of the framed canvas this session builds.
278    pub fn framed_id(&self) -> String {
279        format!("{}-framed", self.source.id)
280    }
281
282    /// The reference frame, or `None` when none is stored.
283    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    /// One stored level-0 tile-index row of the source canvas, in the order the store holds them.
302    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    /// Adds one planned tile; `false` once the plan would exceed the limit.
313    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    /// Plans the perimeter: every tile not wholly inside the content rect. The complement of the tiles fully inside
354    /// is a ring of chrome and background tiles; a content rect whose edges cut through a tile puts that tile here.
355    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    /// Plans the tiles under observed source tiles. Each source tile maps to at most four output tiles: both grids
372    /// share the tile size.
373    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    /// The existing source tiles under output tile `bounds`, row by row.
414    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    /// Takes `key` for the current output tile: touched when resident, `None` (the host must bring it in) when not.
443    /// Before the host brings a tile in, every tile taken so far is copied, since making room may evict it.
444    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            // Inserted by the host just now: already the most recently used tile.
451            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    /// Starts the next planned output tile, or finishes.
533    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    /// Folds the source tiles into the output tile and, when it received any pixel, counts it and asks the host to
596    /// store it.
597    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        // Evicted while its source tiles came in: carry on with the copy taken before that.
604        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                // The output tile's planes leave the cache while the source tiles are read from it.
612                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    /// 40 × 30, every pixel opaque (or, with `alpha` 0, fully transparent) and distinct by position.
671    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    /// A 60 × 50 canvas seen through pane (4, 4, 24, 20): a 76 × 60 framed canvas, 5 × 4 tiles of 16, of which the
688    /// 14 around the edge are chrome and background and the 6 inside are content only.
689    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    /// Every source tile of the 60 × 50 canvas, in the order a store iterates their `x_y` keys.
710    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        // The 14 perimeter tiles fit; the 6 content tiles under the source tiles do not.
910        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            // Top row, bottom row, left column, right column.
938            (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            // Under source tiles 0_0, 0_1, 1_0, 1_1, 2_0, 2_1 (each adds the content tiles not yet planned).
953            (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        // (0,0) covers source 0_0; (1,0) covers 0_0 (resident by then) and 1_0; (2,0) covers 1_0 and 2_0.
1005        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        // One provisional bit per source tile, at its first pixel: all 16 land inside the 60 × 50 canvas.
1054        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}