Skip to main content

long_screen/engine/
render.rs

1//! The render pass (docs/ARCHITECTURE.md §七): every solved placement of every frame resolved to its final canvas
2//! and pose, checked against the frames either side of it, and painted into the tile cache by the [`Compositor`].
3//!
4//! A [`RenderSession`] holds what the pass keeps for its whole length: the region table, the attachment chain, a
5//! three-slot frame ring (previous, current, lookahead), the world-consistency mask, the last-painted pixels of every
6//! fixed region, the last placement of every layer, a four-slot window over solve's voting records, and the canvas
7//! metadata the pass updates. The host drives it one frame at a time:
8//!
9//! 1. [`RenderSession::wanted_votes`] names the `consistency/<frame>` records the window does not hold yet; the host
10//!    reads them and hands them to [`RenderSession::begin`] with the frame's plan and its neighbours' plans.
11//! 2. [`RenderSession::step`] runs the frame's placements in order until it needs something only the host has:
12//!    a canvas's stored metadata ([`RenderStep::NeedMeta`] → [`RenderSession::provide_meta`]), a decoded frame's
13//!    pixels ([`RenderStep::NeedFrame`] → write them into [`RenderSession::frame_slot`]), a tile or a canvas's
14//!    temporal records (the compositor's own needs, passed through), and returns [`RenderStep::Done`] with the
15//!    frame's observation row once every placement is through.
16//! 3. [`RenderSession::take_events`] after every step (also a failed one) returns the diagnostics raised so far,
17//!    in emission order; [`RenderSession::take_dirty_metas`] on the host's flush cadence returns the canvases whose
18//!    metadata changed since the last flush, in first-changed order.
19//!
20//! The session borrows the tile cache, the compositor and the pose graph for each step (a [`RenderIo`]); it owns none
21//! of them, so the host keeps flushing tiles and temporal records exactly as before. It reads no clock: `now` comes
22//! with every step and only stamps the tiles it writes.
23//!
24//! Frames enter the ring only when a placement first needs them, and a new frame takes the slot filled longest ago
25//! among those holding none of the frame in progress and its two neighbours. A placement keeps the slots it was
26//! given while it loads the frames it still lacks, so a slot one of them holds must not be reused. After two frames
27//! that paint nothing, the next frame t loads itself before frame t−1; reusing plainly the slot filled longest ago
28//! then put frame t+2 into frame t's slot while frame t+1 still compared against it, so frame t+1's mask read frame
29//! t+2's pixels in frame t's place.
30
31use crate::engine::attachments::{render_shift, resolve_target, Attachment, Attachments};
32use crate::engine::compositor::{
33    CompositeError, Compositor, CompositorDiagnostic, Frame, Placement, Step as CompositorStep,
34};
35use crate::engine::diagnostic::{Diagnostic, Severity, Value};
36use crate::engine::overscroll::{self, Overscroll};
37use crate::engine::pose_graph::{PoseGraph, PoseGraphError};
38use crate::engine::tiles::{TileCache, TileKey};
39use crate::kernels::consistency::{
40    consistency_mask, fixed_mask, sum_limit, FixedMaskInput, MaskInput, Neighbour, Vote,
41};
42use crate::kernels::fixed_history::FixedHistory;
43use crate::kernels::geometry::{js_round, Rect};
44use crate::kernels::raster::fixed_update;
45use std::collections::{HashMap, HashSet, VecDeque};
46use std::fmt;
47use std::sync::Arc;
48
49/// A fixed region that settled into a new state in more than this share of its frames is not one bar with a few
50/// states but content changing under it (test_8's translucent header: 325 of 599 frames; real bars: at most 83
51/// of 1,258). It keeps its first frame instead of its last state. A short recording needs more
52/// than [`DYNAMIC_FRAMES`] such frames too: one bar appearing in a four-frame clip is not content moving under it.
53const DYNAMIC_SHARE: f64 = 0.2;
54const DYNAMIC_FRAMES: u32 = 10;
55
56pub use crate::engine::solve::{is_valid_pose, POSE_BOUND};
57
58/// Frames the ring holds: the previous, the current and the lookahead frame.
59pub const RING_FRAMES: usize = 3;
60/// Voting records the window keeps: a frame and both neighbours, plus one, so consecutive frames read one new
61/// record each.
62pub const VOTING_WINDOW: usize = 4;
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum RegionKind {
66    Moving,
67    Fixed,
68    Ignore,
69}
70
71/// One region of the run's atlas.
72#[derive(Clone, Debug, PartialEq)]
73pub struct RenderRegion {
74    pub id: String,
75    pub kind: RegionKind,
76    /// The region's rect on screen.
77    pub rect: Rect,
78    /// Its code in the atlas label plane and how many pixels the atlas gives it.
79    pub code: u8,
80    pub owned_pixels: f64,
81}
82
83/// One placement of a stored plan (`plan/<frame>`), in the row's field order. `skip` and `occlusions` are absent
84/// from the row unless set, and stay absent in the observation row built from it.
85#[derive(Clone, Debug, PartialEq)]
86pub struct PlanPlacement {
87    pub layer: String,
88    pub canvas_id: String,
89    pub node: String,
90    pub x: f64,
91    pub y: f64,
92    pub confidence: f64,
93    pub uncertain: bool,
94    pub time: f64,
95    pub skip: Option<bool>,
96    pub occlusions: Option<Vec<Rect>>,
97}
98
99impl PlanPlacement {
100    fn skipped(&self) -> bool {
101        self.skip == Some(true)
102    }
103}
104
105/// What render reads of a stored plan.
106#[derive(Clone, Debug, Default, PartialEq)]
107pub struct FramePlan {
108    pub placements: Vec<PlanPlacement>,
109    pub duplicate: bool,
110}
111
112/// Solve's displacement-spread verdict for one region of one frame (`consistency/<frame>`, docs/ARCHITECTURE.md §七):
113/// the analysis-resolution box it scored and two bitsets over it, LSB first — cells found inconsistent, cells found
114/// confidently consistent.
115#[derive(Clone, Debug, PartialEq)]
116pub struct ConsistencyVote {
117    pub x0: i32,
118    pub y0: i32,
119    pub w: i32,
120    pub h: i32,
121    pub bits: Vec<u8>,
122    pub clean: Vec<u8>,
123}
124
125/// One frame's `consistency/<frame>` record, by region id.
126pub type ConsistencyRecord = HashMap<String, ConsistencyVote>;
127
128/// The fields of a canvas's stored metadata (`canvas/<id>`) the render pass updates; the host keeps the rest of the
129/// row and writes these back into it.
130#[derive(Clone, Copy, Debug, PartialEq)]
131pub struct CanvasMeta {
132    pub bounds: Rect,
133    pub tile_count: f64,
134    pub observed_pixels: f64,
135    pub uncertain_pixels: f64,
136    pub conflict_pixels: f64,
137    pub provisional_pixels: f64,
138    pub last_time: f64,
139}
140
141/// One placement's line in the observation ledger (`observation/<frame>`): the canvas it targeted, its final
142/// render-time placement, and either its pixel contribution or which shortcut skipped painting it.
143#[derive(Clone, Debug, PartialEq)]
144pub struct RenderDecision {
145    pub canvas_id: String,
146    pub placement: PlanPlacement,
147    pub added_pixels: f64,
148    pub conflict_pixels: f64,
149    pub uncertain_pixels: f64,
150    /// Written as `skipped: true` when set, absent otherwise.
151    pub skipped: bool,
152    /// Written as `reusedExactObservation: true` when set, absent otherwise.
153    pub reused_exact_observation: bool,
154}
155
156/// The `observation/<frame>` row.
157#[derive(Clone, Debug, PartialEq)]
158pub struct Observation {
159    pub frame: u32,
160    pub time: f64,
161    pub decisions: Vec<RenderDecision>,
162}
163
164/// What a finished frame leaves the host: its ledger row, and the canvas of the last moving-region placement it
165/// painted or reused (the progress event's canvas), if any.
166#[derive(Clone, Debug, PartialEq)]
167pub struct FrameOutcome {
168    pub observation: Observation,
169    pub latest: Option<String>,
170}
171
172/// The pass-level counters a step changes, cumulative over the session.
173#[derive(Clone, Copy, Debug, Default, PartialEq)]
174pub struct RenderCounters {
175    /// Placements the duplicate or fixed-region-unchanged shortcut did not paint.
176    pub skipped_paints: u64,
177    /// Tiles first painted.
178    pub tiles: f64,
179    /// Pixels first painted.
180    pub observed_pixels: f64,
181}
182
183#[derive(Clone, Debug, PartialEq)]
184pub enum RenderStep {
185    /// The canvas's stored metadata is needed: [`RenderSession::provide_meta`], then step again.
186    NeedMeta(String),
187    /// The frame's pixels are needed: write them into [`RenderSession::frame_slot`], then step again.
188    NeedFrame(u32),
189    /// The compositor needs this tile resident: make room, insert it, step again.
190    NeedTile(TileKey),
191    /// The compositor needs this canvas's stored temporal records: [`Compositor::load_temporal`], step again.
192    NeedTemporal(String),
193    Done(FrameOutcome),
194}
195
196#[derive(Clone, Debug, PartialEq)]
197pub enum RenderError {
198    /// `begin` while a frame is in progress, or `step` without one.
199    Busy,
200    NoFrame,
201    /// The labels or a frame do not have the session's size.
202    FrameSize,
203    /// The host did not hand over a voting record [`RenderSession::wanted_votes`] asked for.
204    MissingVote(i64),
205    /// A voting record's bitsets are shorter than its box, or its box is empty.
206    BadVote,
207    /// A placement names a layer the region table does not hold.
208    UnknownRegion(String),
209    /// The canvas a placement resolved to has no stored metadata.
210    MissingMeta(String),
211    /// A pose graph lookup failed (a placement's or an attachment's node is missing).
212    Graph(PoseGraphError),
213    /// The compositor refused the placement: its rect leaves the addressable canvas.
214    OutsideCanvas {
215        x: f64,
216        y: f64,
217        region: String,
218    },
219    /// The compositor's temporal records for the canvas were already disposed.
220    Disposed(String),
221    /// Any other compositor failure.
222    Compositor(CompositeError),
223}
224
225impl fmt::Display for RenderError {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            RenderError::Busy => f.write_str("A frame is already being rendered."),
229            RenderError::NoFrame => f.write_str("No frame is being rendered."),
230            RenderError::FrameSize => f.write_str("Frame does not match the run's size."),
231            RenderError::MissingVote(i) => write!(f, "Voting record {i} was not provided."),
232            RenderError::BadVote => f.write_str("CORE_BAD_ARGUMENT: truncated consistency vote bitset."),
233            RenderError::UnknownRegion(id) => write!(f, "Region {id} is not part of this atlas."),
234            RenderError::MissingMeta(_) => f.write_str("Canvas metadata is missing."),
235            RenderError::Graph(e) => e.fmt(f),
236            RenderError::OutsideCanvas { x, y, region } => write!(
237                f,
238                "Placement ({x}, {y}) puts region {region} outside the addressable canvas."
239            ),
240            RenderError::Disposed(canvas) => write!(
241                f,
242                "CORE_BAD_ARGUMENT: temporalIndex({canvas}) called after dispose() already drained and froze its state."
243            ),
244            RenderError::Compositor(e) => write!(f, "Compositor failed: {e:?}."),
245        }
246    }
247}
248
249impl std::error::Error for RenderError {}
250
251impl From<PoseGraphError> for RenderError {
252    fn from(e: PoseGraphError) -> Self {
253        RenderError::Graph(e)
254    }
255}
256
257/// What the session borrows for one step.
258pub struct RenderIo<'a> {
259    pub cache: &'a mut TileCache,
260    pub compositor: &'a mut Compositor,
261    pub graph: &'a PoseGraph,
262    /// The atlas label plane, frame-sized.
263    pub labels: &'a [u8],
264    /// The host's clock, stamped on every tile this step writes.
265    pub now: f64,
266}
267
268/// What the host knows about one frame when it composites it.
269#[derive(Clone, Debug, PartialEq)]
270pub struct FrameInput {
271    pub index: u32,
272    /// The decoded frame's time (the observation row's `time`).
273    pub time: f64,
274    pub plan: FramePlan,
275    /// Whether the previous / next decoded frame is in hand, and its stored plan when there is one.
276    pub has_prev: bool,
277    pub prev_plan: Option<FramePlan>,
278    pub has_next: bool,
279    pub next_plan: Option<FramePlan>,
280}
281
282/// Run-wide constants of a session.
283#[derive(Clone, Debug, PartialEq)]
284pub struct RenderConfig {
285    pub width: usize,
286    pub height: usize,
287    /// Integer analysis factor: analysis pixels × factor = native pixels.
288    pub factor: i32,
289    /// Per-channel decode noise the source declares; the mask's tolerance.
290    pub noise: f64,
291    /// The atlas's regions, in atlas order.
292    pub regions: Vec<RenderRegion>,
293    pub attachments: Vec<Attachment>,
294}
295
296/// A placement resolved to its final canvas and pose.
297#[derive(Clone, Debug, PartialEq)]
298struct Resolved {
299    canvas: String,
300    x: f64,
301    y: f64,
302}
303
304/// One slot of the frame ring: which frame it holds and when that frame entered.
305struct Slot {
306    pixels: Vec<u8>,
307    index: i64,
308    used: u64,
309}
310
311/// Resident frames keyed by frame index. A new frame takes the slot filled longest ago that holds none of the frames
312/// the frame in progress compares (see [`FrameRing::fill`]); reading a frame does not change that order.
313struct FrameRing {
314    slots: Vec<Slot>,
315    tick: u64,
316    bytes: usize,
317}
318
319impl FrameRing {
320    fn new(bytes: usize) -> Self {
321        Self {
322            slots: Vec::with_capacity(RING_FRAMES),
323            tick: 0,
324            bytes,
325        }
326    }
327
328    fn get(&self, index: i64) -> Option<usize> {
329        self.slots.iter().position(|s| s.index == index)
330    }
331
332    /// The slot frame `index` goes into. While a frame is being composited, `keep` is the first and last frame it
333    /// compares (its previous and next): a slot holding one of those is never reused, because a placement reads
334    /// the slots it was given after loading the frames it still lacks. With three slots and three such frames,
335    /// a frame that is not resident always leaves a slot outside `keep`.
336    fn fill(&mut self, index: i64, keep: Option<(i64, i64)>) -> usize {
337        if let Some(slot) = self.get(index) {
338            return slot;
339        }
340        let slot = if self.slots.len() < RING_FRAMES {
341            self.slots.push(Slot {
342                pixels: vec![0; self.bytes],
343                index,
344                used: 0,
345            });
346            self.slots.len() - 1
347        } else {
348            let kept = |s: &Slot| keep.is_some_and(|(a, b)| (a..=b).contains(&s.index));
349            let oldest = |free: bool| {
350                (0..self.slots.len())
351                    .filter(|&i| !free || !kept(&self.slots[i]))
352                    .min_by_key(|&i| self.slots[i].used)
353            };
354            oldest(true).or_else(|| oldest(false)).unwrap()
355        };
356        self.tick += 1;
357        let s = &mut self.slots[slot];
358        s.index = index;
359        s.used = self.tick;
360        slot
361    }
362}
363
364/// The last few frames' voting records, newest last; a missing record is kept as `None` like a present one.
365#[derive(Default)]
366struct VotingWindow {
367    entries: VecDeque<(i64, Option<Arc<ConsistencyRecord>>)>,
368}
369
370impl VotingWindow {
371    /// The indices of `gets`, in order, that the window will not hold when they are asked for (negative indices
372    /// are never stored rows, so they are not wanted).
373    fn wanted(&self, gets: &[i64]) -> Vec<i64> {
374        let mut held: VecDeque<i64> = self.entries.iter().map(|(i, _)| *i).collect();
375        let mut wanted = Vec::new();
376        for &index in gets {
377            if held.contains(&index) {
378                continue;
379            }
380            if index >= 0 {
381                wanted.push(index);
382            }
383            held.push_back(index);
384            if held.len() > VOTING_WINDOW {
385                held.pop_front();
386            }
387        }
388        wanted
389    }
390
391    fn get(
392        &mut self,
393        index: i64,
394        provided: &mut HashMap<i64, Option<ConsistencyRecord>>,
395    ) -> Result<Option<Arc<ConsistencyRecord>>, RenderError> {
396        if let Some((_, record)) = self.entries.iter().find(|(i, _)| *i == index) {
397            return Ok(record.clone());
398        }
399        let record = if index < 0 {
400            None
401        } else {
402            provided
403                .remove(&index)
404                .ok_or(RenderError::MissingVote(index))?
405                .map(Arc::new)
406        };
407        self.entries.push_back((index, record.clone()));
408        if self.entries.len() > VOTING_WINDOW {
409            self.entries.pop_front();
410        }
411        Ok(record)
412    }
413}
414
415/// A neighbour frame's placement for the same layer, resolved, and the ring slot its pixels went into.
416struct NeighbourPlacement {
417    resolved: Resolved,
418    occlusions: Vec<Rect>,
419    slot: Option<usize>,
420}
421
422/// Where the placement in progress is.
423enum Stage {
424    /// Not started: skip, resolve, validate.
425    Start,
426    /// Resolved; its canvas metadata is needed.
427    Meta(Resolved),
428    /// A fixed region whose plan-level shortcut did not apply: compare against the saved copy.
429    Fixed(Resolved),
430    /// Shortcuts decided.
431    Decide(Resolved, bool),
432    /// Painting: the current frame, then (moving regions) both neighbours and the mask, then the compositor.
433    Paint(Paint),
434}
435
436struct Paint {
437    p: Resolved,
438    current: Option<usize>,
439    /// For a moving region: the neighbours, resolved once the current frame is in (`None` until then).
440    neighbours: Option<[Option<NeighbourPlacement>; 2]>,
441    masked: bool,
442    /// Lines left out past the page's edges ([`crate::kernels::overscroll::SIDES`] order), once measured.
443    overscroll: Option<[u16; 4]>,
444    begun: bool,
445}
446
447struct FrameJob {
448    input: FrameInput,
449    votes: [Option<Arc<ConsistencyRecord>>; 3],
450    placement: usize,
451    stage: Stage,
452    decisions: Vec<RenderDecision>,
453    latest: Option<String>,
454}
455
456/// What the session knows about one fixed region's states over the run.
457struct FixedState {
458    history: FixedHistory,
459    /// Frames that placed the region, and those of them that settled some block into a new state.
460    frames: u32,
461    settled: u32,
462    /// Once the run is finished: the first frame without transients (as it was, for a dynamic region).
463    first: Option<Vec<u32>>,
464}
465
466/// The finishing job: dynamic fixed regions, whose canvas takes their first frame back.
467struct FinishJob {
468    regions: Vec<usize>,
469    at: usize,
470    begun: bool,
471    frame: u32,
472    time: f64,
473}
474
475pub struct RenderSession {
476    width: usize,
477    height: usize,
478    factor: i32,
479    noise: f64,
480    regions: Vec<RenderRegion>,
481    region_index: HashMap<String, usize>,
482    attachments: Attachments,
483    ring: FrameRing,
484    mask: Vec<u8>,
485    fixed: HashMap<String, Vec<u8>>,
486    states: HashMap<String, FixedState>,
487    finish: Option<FinishJob>,
488    last: Option<(u32, f64)>,
489    previous: HashMap<String, Resolved>,
490    votes: VotingWindow,
491    metas: HashMap<String, CanvasMeta>,
492    missing_meta: Option<String>,
493    dirty: Vec<String>,
494    dirty_set: HashSet<String>,
495    counters: RenderCounters,
496    events: Vec<Diagnostic>,
497    job: Option<FrameJob>,
498    overscroll: Overscroll,
499}
500
501impl RenderSession {
502    pub fn new(config: RenderConfig) -> Self {
503        let mut region_index = HashMap::new();
504        for (i, r) in config.regions.iter().enumerate() {
505            region_index.insert(r.id.clone(), i);
506        }
507        let attachments: Attachments = config
508            .attachments
509            .into_iter()
510            .map(|a| (a.id.clone(), a))
511            .collect();
512        let pixels = config.width * config.height;
513        Self {
514            width: config.width,
515            height: config.height,
516            factor: config.factor,
517            noise: config.noise,
518            regions: config.regions,
519            region_index,
520            attachments,
521            ring: FrameRing::new(pixels * 4),
522            mask: vec![1; pixels],
523            fixed: HashMap::new(),
524            states: HashMap::new(),
525            finish: None,
526            last: None,
527            previous: HashMap::new(),
528            votes: VotingWindow::default(),
529            metas: HashMap::new(),
530            missing_meta: None,
531            dirty: Vec::new(),
532            dirty_set: HashSet::new(),
533            counters: RenderCounters::default(),
534            events: Vec::new(),
535            job: None,
536            overscroll: Overscroll::default(),
537        }
538    }
539
540    /// Hands over a stored plan before the first frame is rendered: the session reads every plan's moving-region
541    /// placements to find where each canvas's page ends (elastic overscroll, docs/ARCHITECTURE.md §十). A host that
542    /// hands over none renders as if no page had edges.
543    pub fn add_plan(&mut self, index: u32, plan: &FramePlan) {
544        let (regions, index_of) = (&self.regions, &self.region_index);
545        self.overscroll.add_plan(index, plan, |layer| {
546            index_of
547                .get(layer)
548                .is_some_and(|&i| regions[i].kind == RegionKind::Moving)
549        });
550    }
551
552    /// Sizes the tile cache for this pass: the memory setting less what the pass itself holds (the ring's frames
553    /// and the frame-sized planes around them, each fixed region's saved copy and history, 8 MB of headroom), then
554    /// raised to what one frame paints. Returns the `MEMORY_BUDGET_RAISED` diagnostic (info) when it had to raise the limit:
555    /// `from`, `to` and `mb` for the message, and a `{ budgetMB, tiles, raisedBy }` detail.
556    pub fn configure_tiles(&self, cache: &mut TileCache, memory_mb: f64) -> Option<Diagnostic> {
557        let fixed_bytes = self
558            .regions
559            .iter()
560            .filter(|r| r.kind == RegionKind::Fixed)
561            .fold(0.0, |n, r| {
562                n + r.rect.width.ceil() * r.rect.height.ceil() * 4.0 + FixedHistory::bytes(r.rect)
563            });
564        let (w, h) = (self.width as f64, self.height as f64);
565        cache.configure_budget(
566            memory_mb,
567            w * h * 5.0 + fixed_bytes + 8.0 * 1024.0 * 1024.0 + w * h * 4.0,
568        );
569        let areas: Vec<(f64, f64)> = self
570            .regions
571            .iter()
572            .filter(|r| r.kind != RegionKind::Ignore)
573            .map(|r| (r.rect.width, r.rect.height))
574            .collect();
575        let raised = cache.ensure_footprint(&areas);
576        if raised == 0 {
577            return None;
578        }
579        let to = cache.max_tiles();
580        let size = cache.size() as f64;
581        Some(
582            Diagnostic::new("MEMORY_BUDGET_RAISED", Severity::Info)
583                .param("from", (to - raised) as f64)
584                .param("to", to as f64)
585                .param(
586                    "mb",
587                    js_round(to as f64 * size * size * 4.3 / 1024.0 / 1024.0) as f64,
588                )
589                .detail(Value::Object(vec![
590                    ("budgetMB", Value::Number(memory_mb)),
591                    ("tiles", Value::Number(to as f64)),
592                    ("raisedBy", Value::Number(raised as f64)),
593                ])),
594        )
595    }
596
597    pub fn counters(&self) -> RenderCounters {
598        self.counters
599    }
600
601    /// The canvas's metadata as the pass holds it now, if it has been loaded.
602    pub fn meta(&self, canvas: &str) -> Option<CanvasMeta> {
603        self.metas.get(canvas).copied()
604    }
605
606    /// Diagnostics raised since the last call, in emission order: `NONFINITE_POSE` (error; a placement's resolved
607    /// pose cannot be placed, it is recorded as skipped at (0, 0)), and the compositor's `INCOMPLETE_TEMPORAL_PATCH`
608    /// and `TEMPORAL_OR_ALIGNMENT_CONFLICT` (warnings; `conflicts` the pixels detected before patching, every
609    /// conflicting rect in `detail.regions`).
610    pub fn take_events(&mut self) -> Vec<Diagnostic> {
611        std::mem::take(&mut self.events)
612    }
613
614    /// Canvases whose metadata changed since the last call, in the order they first changed, with their current
615    /// values.
616    pub fn take_dirty_metas(&mut self) -> Vec<(String, CanvasMeta)> {
617        self.dirty_set.clear();
618        std::mem::take(&mut self.dirty)
619            .into_iter()
620            .map(|id| {
621                let meta = self.metas[&id];
622                (id, meta)
623            })
624            .collect()
625    }
626
627    /// The `consistency/<frame>` records the host should read for frame `index` (in this order) before
628    /// [`RenderSession::begin`]: the frame's own, then its previous and next frame's when those are in hand,
629    /// leaving out what the window still holds.
630    pub fn wanted_votes(&self, index: u32, has_prev: bool, has_next: bool) -> Vec<i64> {
631        self.votes.wanted(&vote_gets(index, has_prev, has_next))
632    }
633
634    /// Starts compositing one frame. `votes` are the records [`RenderSession::wanted_votes`] named, `None` where
635    /// no row is stored.
636    pub fn begin(
637        &mut self,
638        input: FrameInput,
639        votes: Vec<(i64, Option<ConsistencyRecord>)>,
640    ) -> Result<(), RenderError> {
641        if self.job.is_some() || self.finish.is_some() {
642            return Err(RenderError::Busy);
643        }
644        self.last = Some((input.index, input.time));
645        let mut provided: HashMap<i64, Option<ConsistencyRecord>> = votes.into_iter().collect();
646        let index = input.index as i64;
647        let own = self.votes.get(index, &mut provided)?;
648        let prev = if input.has_prev {
649            self.votes.get(index - 1, &mut provided)?
650        } else {
651            None
652        };
653        let next = if input.has_next {
654            self.votes.get(index + 1, &mut provided)?
655        } else {
656            None
657        };
658        self.job = Some(FrameJob {
659            input,
660            votes: [own, prev, next],
661            placement: 0,
662            stage: Stage::Start,
663            decisions: Vec::new(),
664            latest: None,
665        });
666        Ok(())
667    }
668
669    /// Abandons the frame in progress (the host failed or stopped between steps), and the compositor's placement
670    /// with it. What the frame already changed (metadata, saved fixed-region pixels, last placements) stays, as it
671    /// did when the pass failed mid-frame before.
672    pub fn cancel(&mut self, compositor: &mut Compositor) {
673        if self.job.take().is_some() {
674            compositor.cancel();
675        }
676        self.missing_meta = None;
677    }
678
679    /// Hands over the metadata of the canvas [`RenderStep::NeedMeta`] asked for; `None` when no row is stored (the
680    /// next step then fails).
681    pub fn provide_meta(&mut self, canvas: &str, meta: Option<CanvasMeta>) {
682        match meta {
683            Some(meta) => {
684                self.metas.insert(canvas.to_string(), meta);
685            }
686            None => self.missing_meta = Some(canvas.to_string()),
687        }
688    }
689
690    /// The ring slot frame `index` goes into, for the host to write its RGBA after [`RenderStep::NeedFrame`]. The
691    /// slot filled longest ago is reused, but never one holding the frame in progress or either of its neighbours.
692    pub fn frame_slot(&mut self, index: u32) -> &mut [u8] {
693        let keep = self.job.as_ref().map(|j| {
694            let at = j.input.index as i64;
695            (at - 1, at + 1)
696        });
697        let slot = self.ring.fill(index as i64, keep);
698        &mut self.ring.slots[slot].pixels
699    }
700
701    /// Runs the frame until it needs something from the host or is done. On an error the frame is abandoned (and
702    /// the compositor's placement cancelled); the session stays usable.
703    pub fn step(&mut self, io: &mut RenderIo) -> Result<RenderStep, RenderError> {
704        if self.finish.is_some() {
705            let result = self.run_finish(io);
706            if result.is_err() {
707                self.finish = None;
708                io.compositor.cancel();
709            }
710            return result;
711        }
712        let mut job = self.job.take().ok_or(RenderError::NoFrame)?;
713        let result = if io.labels.len() != self.width * self.height {
714            Err(RenderError::FrameSize)
715        } else {
716            self.run(&mut job, io)
717        };
718        match result {
719            Ok(RenderStep::Done(_)) => {}
720            Ok(_) => self.job = Some(job),
721            Err(_) => {
722                if matches!(job.stage, Stage::Paint(Paint { begun: true, .. })) {
723                    io.compositor.cancel();
724                }
725                self.missing_meta = None;
726            }
727        }
728        result
729    }
730
731    /// Ends the run's fixed regions after the last frame: each one's first epoch closes and its first frame without
732    /// transients is worked out, and a region whose state kept changing (more than [`DYNAMIC_SHARE`] of its frames,
733    /// and more than [`DYNAMIC_FRAMES`], settled into a new state) gets its first frame back on its canvas, as it
734    /// was, instead of its last state. Returns whether there is anything to paint;
735    /// when there is, step the session as for a frame until [`RenderStep::Done`] (its observation is empty and is
736    /// not a ledger row). Call once, after the last frame.
737    pub fn begin_finish(&mut self) -> Result<bool, RenderError> {
738        if self.job.is_some() || self.finish.is_some() {
739            return Err(RenderError::Busy);
740        }
741        let Some((last, time)) = self.last else {
742            return Ok(false);
743        };
744        let limit = sum_limit(self.noise);
745        let mut regions = Vec::new();
746        for (at, region) in self.regions.iter().enumerate() {
747            let Some(state) = self.states.get_mut(&region.id) else {
748                continue;
749            };
750            if state.first.is_some() {
751                continue;
752            }
753            state.history.close(last + 1);
754            let dynamic = state.settled > DYNAMIC_FRAMES
755                && state.settled as f64 > DYNAMIC_SHARE * state.frames as f64;
756            // Content moving under a dynamic region also changes in small patches; none of them is a transient
757            // over the bar, so its first frame is kept as it was.
758            state.first = Some(if dynamic {
759                state.history.frame0().to_vec()
760            } else {
761                state.history.first(limit)
762            });
763            if dynamic && self.previous.contains_key(&region.id) {
764                regions.push(at);
765            }
766        }
767        if regions.is_empty() {
768            return Ok(false);
769        }
770        let frame = self.states[&self.regions[regions[0]].id]
771            .history
772            .first_frame()
773            .unwrap_or(0);
774        self.finish = Some(FinishJob {
775            regions,
776            at: 0,
777            begun: false,
778            frame,
779            time,
780        });
781        Ok(true)
782    }
783
784    /// Paints every fixed region's first frame without transients into `rgba` (`width × height`, the reference frame
785    /// `frame`), after [`RenderSession::begin_finish`]; returns whether any pixel changed. Regions whose first
786    /// observation was another frame are left alone.
787    pub fn patch_reference(
788        &self,
789        rgba: &mut [u8],
790        width: usize,
791        height: usize,
792        frame: u32,
793    ) -> bool {
794        if width != self.width || height != self.height || rgba.len() != width * height * 4 {
795            return false;
796        }
797        let mut changed = false;
798        for region in &self.regions {
799            let Some(state) = self.states.get(&region.id) else {
800                continue;
801            };
802            if let (Some(first), Some(from)) = (&state.first, state.history.first_frame()) {
803                if from == frame {
804                    changed |= state.history.paint(first, rgba, width, height);
805                }
806            }
807        }
808        changed
809    }
810
811    fn run_finish(&mut self, io: &mut RenderIo) -> Result<RenderStep, RenderError> {
812        loop {
813            let job = self.finish.as_mut().unwrap();
814            let Some(&at) = job.regions.get(job.at) else {
815                let (frame, time) = (job.frame, job.time);
816                self.finish = None;
817                return Ok(RenderStep::Done(FrameOutcome {
818                    observation: Observation {
819                        frame,
820                        time,
821                        decisions: Vec::new(),
822                    },
823                    latest: None,
824                }));
825            };
826            let (begun, frame_index, time) = (job.begun, job.frame, job.time);
827            let region = &self.regions[at];
828            let p = self.previous[&region.id].clone();
829            let slot = self.ring.fill(-2, None);
830            if !begun {
831                let state = &self.states[&region.id];
832                state.history.paint(
833                    state.first.as_ref().unwrap(),
834                    &mut self.ring.slots[slot].pixels,
835                    self.width,
836                    self.height,
837                );
838                self.mask.fill(1);
839                io.compositor
840                    .begin(
841                        io.cache,
842                        Placement {
843                            canvas: p.canvas.clone(),
844                            region: region.rect,
845                            code: region.code,
846                            owned_pixels: region.owned_pixels,
847                            x: p.x,
848                            y: p.y,
849                            frame: frame_index,
850                            time,
851                            confidence: 1.0,
852                            uncertain: false,
853                            occlusions: Vec::new(),
854                            fixed: true,
855                        },
856                        self.width,
857                        self.height,
858                    )
859                    .map_err(RenderError::Compositor)?;
860                self.finish.as_mut().unwrap().begun = true;
861            }
862            let frame = Frame {
863                rgba: &self.ring.slots[slot].pixels,
864                width: self.width,
865                height: self.height,
866                labels: io.labels,
867                consistent: Some(&self.mask[..]),
868            };
869            let outcome = match io.compositor.step(io.cache, &frame, io.now) {
870                Ok(CompositorStep::NeedTile(key)) => return Ok(RenderStep::NeedTile(key)),
871                Ok(CompositorStep::NeedTemporal(canvas)) => {
872                    return Ok(RenderStep::NeedTemporal(canvas))
873                }
874                Ok(CompositorStep::Done(outcome)) => outcome,
875                Err(e) => return Err(RenderError::Compositor(e)),
876            };
877            // What the canvas held is replaced, not contradicted: the rewrite reports no conflict of its own.
878            if let Some(meta) = self.metas.get_mut(&p.canvas) {
879                meta.observed_pixels += outcome.stats.added as f64;
880                meta.provisional_pixels += outcome.stats.provisional_pixels as f64;
881            }
882            self.mark_dirty(&p.canvas);
883            let job = self.finish.as_mut().unwrap();
884            job.begun = false;
885            job.at += 1;
886        }
887    }
888
889    fn resolve(
890        &self,
891        raw: &PlanPlacement,
892        frame: f64,
893        graph: &PoseGraph,
894    ) -> Result<Resolved, RenderError> {
895        let (cx, cy) = graph.correction(&raw.node, frame)?;
896        let (sx, sy) = render_shift(&self.attachments, &raw.canvas_id, graph)?;
897        let canvas = resolve_target(&self.attachments, &raw.canvas_id).to_string();
898        Ok(Resolved {
899            canvas,
900            x: raw.x + cx + sx,
901            y: raw.y + cy + sy,
902        })
903    }
904
905    fn region(&self, layer: &str) -> Result<usize, RenderError> {
906        self.region_index
907            .get(layer)
908            .copied()
909            .ok_or_else(|| RenderError::UnknownRegion(layer.to_string()))
910    }
911
912    fn mark_dirty(&mut self, canvas: &str) {
913        if self.dirty_set.insert(canvas.to_string()) {
914            self.dirty.push(canvas.to_string());
915        }
916    }
917
918    fn run(&mut self, job: &mut FrameJob, io: &mut RenderIo) -> Result<RenderStep, RenderError> {
919        loop {
920            let Some(raw) = job.input.plan.placements.get(job.placement) else {
921                return Ok(RenderStep::Done(FrameOutcome {
922                    observation: Observation {
923                        frame: job.input.index,
924                        time: job.input.time,
925                        decisions: std::mem::take(&mut job.decisions),
926                    },
927                    latest: job.latest.take(),
928                }));
929            };
930            let raw = raw.clone();
931            let index = job.input.index;
932            match std::mem::replace(&mut job.stage, Stage::Start) {
933                Stage::Start => {
934                    if raw.skipped() {
935                        job.decisions
936                            .push(decision(raw.canvas_id.clone(), raw, None));
937                        job.placement += 1;
938                        continue;
939                    }
940                    let p = self.resolve(&raw, index as f64, io.graph)?;
941                    if !is_valid_pose(p.x, p.y) {
942                        self.events.push(
943                            Diagnostic::new("NONFINITE_POSE", Severity::Error)
944                                .at(index, raw.time)
945                                .canvas(&p.canvas),
946                        );
947                        let placement = PlanPlacement {
948                            canvas_id: p.canvas.clone(),
949                            x: 0.0,
950                            y: 0.0,
951                            ..raw
952                        };
953                        job.decisions.push(decision(p.canvas, placement, None));
954                        job.placement += 1;
955                        continue;
956                    }
957                    job.stage = Stage::Meta(p);
958                }
959                Stage::Meta(p) => {
960                    if !self.metas.contains_key(&p.canvas) {
961                        if self.missing_meta.take().as_deref() == Some(p.canvas.as_str()) {
962                            return Err(RenderError::MissingMeta(p.canvas));
963                        }
964                        let canvas = p.canvas.clone();
965                        job.stage = Stage::Meta(p);
966                        return Ok(RenderStep::NeedMeta(canvas));
967                    }
968                    let region = &self.regions[self.region(&raw.layer)?];
969                    let unchanged = job.input.plan.duplicate
970                        && self.previous.get(&raw.layer).is_some_and(|last| *last == p);
971                    job.stage = if region.kind == RegionKind::Fixed && !unchanged {
972                        Stage::Fixed(p)
973                    } else {
974                        Stage::Decide(p, unchanged)
975                    };
976                }
977                Stage::Fixed(p) => {
978                    let Some(slot) = self.ring.get(index as i64) else {
979                        job.stage = Stage::Fixed(p);
980                        return Ok(RenderStep::NeedFrame(index));
981                    };
982                    let region = &self.regions[self.region(&raw.layer)?];
983                    let (rw, rh) = (
984                        region.rect.width.ceil() as usize,
985                        region.rect.height.ceil() as usize,
986                    );
987                    let old = self.fixed.contains_key(&region.id);
988                    let saved = self
989                        .fixed
990                        .entry(region.id.clone())
991                        .or_insert_with(|| vec![0; rw * rh * 4]);
992                    let changed = fixed_update(
993                        saved,
994                        &self.ring.slots[slot].pixels,
995                        io.labels,
996                        self.width,
997                        self.height,
998                        region.rect.x.floor() as i64,
999                        region.rect.y.floor() as i64,
1000                        rw,
1001                        rh,
1002                        region.code,
1003                    );
1004                    if !old {
1005                        let region = &self.regions[self.region(&raw.layer)?];
1006                        self.states.insert(
1007                            region.id.clone(),
1008                            FixedState {
1009                                history: FixedHistory::new(region.rect),
1010                                frames: 0,
1011                                settled: 0,
1012                                first: None,
1013                            },
1014                        );
1015                    }
1016                    job.stage = Stage::Decide(p, old && !changed);
1017                }
1018                Stage::Decide(p, unchanged) => {
1019                    self.previous.insert(raw.layer.clone(), p.clone());
1020                    if let Some(state) = self.states.get_mut(&raw.layer) {
1021                        state.frames += 1;
1022                    }
1023                    if !unchanged {
1024                        job.stage = Stage::Paint(Paint {
1025                            p,
1026                            current: None,
1027                            neighbours: None,
1028                            masked: false,
1029                            overscroll: None,
1030                            begun: false,
1031                        });
1032                        continue;
1033                    }
1034                    let moving = self.regions[self.region(&raw.layer)?].kind == RegionKind::Moving;
1035                    self.counters.skipped_paints += 1;
1036                    if let Some(meta) = self.metas.get_mut(&p.canvas) {
1037                        meta.last_time = raw.time;
1038                    }
1039                    self.mark_dirty(&p.canvas);
1040                    if moving {
1041                        job.latest = Some(p.canvas.clone());
1042                    }
1043                    let at = overscroll::Resolved {
1044                        canvas: &p.canvas,
1045                        x: p.x,
1046                        y: p.y,
1047                    };
1048                    let left_out = self.overscroll.reuse(&raw.layer, at);
1049                    let region = self.regions[self.region(&raw.layer)?].rect;
1050                    let placement = placed(&raw, &p, overscroll::strips(region, left_out));
1051                    let mut d = decision(p.canvas, placement, Some((0.0, 0.0, 0.0)));
1052                    d.reused_exact_observation = true;
1053                    job.decisions.push(d);
1054                    job.placement += 1;
1055                }
1056                Stage::Paint(mut paint) => {
1057                    let step = self.paint(job, &raw, &mut paint, io);
1058                    match step {
1059                        Ok(None) => {
1060                            job.placement += 1;
1061                        }
1062                        Ok(Some(need)) => {
1063                            job.stage = Stage::Paint(paint);
1064                            return Ok(need);
1065                        }
1066                        Err(e) => {
1067                            job.stage = Stage::Paint(paint);
1068                            return Err(e);
1069                        }
1070                    }
1071                }
1072            }
1073        }
1074    }
1075
1076    /// One painted placement; `Some` is a need to hand to the host, `None` that the placement is finished.
1077    fn paint(
1078        &mut self,
1079        job: &mut FrameJob,
1080        raw: &PlanPlacement,
1081        paint: &mut Paint,
1082        io: &mut RenderIo,
1083    ) -> Result<Option<RenderStep>, RenderError> {
1084        let index = job.input.index;
1085        let region_at = self.region(&raw.layer)?;
1086        let current = match paint.current {
1087            Some(slot) => slot,
1088            None => {
1089                let Some(slot) = self.ring.get(index as i64) else {
1090                    return Ok(Some(RenderStep::NeedFrame(index)));
1091                };
1092                paint.current = Some(slot);
1093                slot
1094            }
1095        };
1096        let kind = self.regions[region_at].kind;
1097        let moving = kind == RegionKind::Moving;
1098        let fixed = kind == RegionKind::Fixed;
1099        if (moving || fixed) && !paint.masked {
1100            if paint.neighbours.is_none() {
1101                paint.neighbours = Some(self.neighbours(job, raw, io.graph)?);
1102            }
1103            let neighbours = paint.neighbours.as_mut().unwrap();
1104            for (side, n) in neighbours.iter_mut().enumerate() {
1105                let Some(n) = n else { continue };
1106                if n.slot.is_none() {
1107                    let at = if side == 0 { index - 1 } else { index + 1 };
1108                    match self.ring.get(at as i64) {
1109                        Some(slot) => n.slot = Some(slot),
1110                        None => return Ok(Some(RenderStep::NeedFrame(at))),
1111                    }
1112                }
1113            }
1114            // Loading the next frame left the previous frame's slot alone (see `FrameRing::fill`).
1115            debug_assert!(neighbours.iter().enumerate().all(|(side, n)| {
1116                n.as_ref().is_none_or(|n| {
1117                    self.ring.slots[n.slot.unwrap()].index == index as i64 + 2 * side as i64 - 1
1118                })
1119            }));
1120            let region = &self.regions[region_at];
1121            if fixed {
1122                // The same screen pixels of the neighbouring frames that placed the region on this canvas.
1123                let [prev, next] = [0, 1].map(|side| {
1124                    neighbours[side]
1125                        .as_ref()
1126                        .filter(|n| n.resolved.canvas == paint.p.canvas)
1127                        .map(|n| &self.ring.slots[n.slot.unwrap()].pixels[..])
1128                });
1129                fixed_mask(
1130                    &FixedMaskInput {
1131                        rgba: &self.ring.slots[current].pixels,
1132                        labels: io.labels,
1133                        width: self.width,
1134                        height: self.height,
1135                        region: region.rect,
1136                        code: region.code,
1137                        prev,
1138                        next,
1139                        noise: self.noise,
1140                    },
1141                    &mut self.mask,
1142                );
1143                if let Some(state) = self.states.get_mut(&region.id) {
1144                    state.history.update(
1145                        &self.ring.slots[current].pixels,
1146                        io.labels,
1147                        &self.mask,
1148                        self.width,
1149                        self.height,
1150                        region.code,
1151                        index,
1152                        sum_limit(self.noise),
1153                    );
1154                }
1155                paint.masked = true;
1156            }
1157        }
1158        if moving && !paint.masked {
1159            let neighbours = paint.neighbours.as_ref().unwrap();
1160            let region = &self.regions[region_at];
1161            let own = vote(job.votes[0].as_deref(), &region.id)?;
1162            let mut sides: [Option<Neighbour>; 2] = [None, None];
1163            for (side, n) in neighbours.iter().enumerate() {
1164                let Some(n) = n else { continue };
1165                if n.resolved.canvas != paint.p.canvas {
1166                    continue;
1167                }
1168                sides[side] = Some(Neighbour {
1169                    rgba: &self.ring.slots[n.slot.unwrap()].pixels,
1170                    pose_x: n.resolved.x,
1171                    pose_y: n.resolved.y,
1172                    occlusions: &n.occlusions,
1173                    vote: vote(job.votes[side + 1].as_deref(), &region.id)?,
1174                });
1175            }
1176            let [prev, next] = sides;
1177            consistency_mask(
1178                &MaskInput {
1179                    rgba: &self.ring.slots[current].pixels,
1180                    labels: io.labels,
1181                    width: self.width,
1182                    height: self.height,
1183                    region: region.rect,
1184                    code: region.code,
1185                    pose_x: paint.p.x,
1186                    pose_y: paint.p.y,
1187                    prev,
1188                    next,
1189                    vote: own,
1190                    factor: self.factor,
1191                    noise: self.noise,
1192                },
1193                &mut self.mask,
1194            );
1195            paint.masked = true;
1196        }
1197        if paint.overscroll.is_none() {
1198            let lines = self.left_out(raw, paint, region_at, current, io)?;
1199            paint.overscroll = Some(lines.map(|n| n.clamp(0, u16::MAX as i64) as u16));
1200        }
1201        let left_out = paint.overscroll.unwrap_or_default().map(i64::from);
1202        let strips = overscroll::strips(self.regions[region_at].rect, left_out);
1203        let region = &self.regions[region_at];
1204        if !paint.begun {
1205            let mut occlusions = raw.occlusions.clone().unwrap_or_default();
1206            occlusions.extend(strips.iter().copied());
1207            io.compositor
1208                .begin(
1209                    io.cache,
1210                    Placement {
1211                        canvas: paint.p.canvas.clone(),
1212                        region: region.rect,
1213                        code: region.code,
1214                        owned_pixels: region.owned_pixels,
1215                        x: paint.p.x,
1216                        y: paint.p.y,
1217                        frame: index,
1218                        time: raw.time,
1219                        confidence: raw.confidence,
1220                        uncertain: raw.uncertain,
1221                        occlusions,
1222                        fixed,
1223                    },
1224                    self.width,
1225                    self.height,
1226                )
1227                .map_err(|e| match e {
1228                    CompositeError::OutsideCanvas => RenderError::OutsideCanvas {
1229                        x: paint.p.x,
1230                        y: paint.p.y,
1231                        region: region.id.clone(),
1232                    },
1233                    e => RenderError::Compositor(e),
1234                })?;
1235            paint.begun = true;
1236        }
1237        debug_assert_eq!(self.ring.slots[current].index, index as i64);
1238        let frame = Frame {
1239            rgba: &self.ring.slots[current].pixels,
1240            width: self.width,
1241            height: self.height,
1242            labels: io.labels,
1243            consistent: (moving || fixed).then_some(&self.mask[..]),
1244        };
1245        let outcome = match io.compositor.step(io.cache, &frame, io.now) {
1246            Ok(CompositorStep::NeedTile(key)) => return Ok(Some(RenderStep::NeedTile(key))),
1247            Ok(CompositorStep::NeedTemporal(canvas)) => {
1248                return Ok(Some(RenderStep::NeedTemporal(canvas)))
1249            }
1250            Ok(CompositorStep::Done(outcome)) => outcome,
1251            Err(CompositeError::Disposed) => {
1252                return Err(RenderError::Disposed(paint.p.canvas.clone()))
1253            }
1254            Err(e) => return Err(RenderError::Compositor(e)),
1255        };
1256        paint.begun = false;
1257        self.events
1258            .extend(outcome.diagnostics.into_iter().map(compositor_diagnostic));
1259        let s = outcome.stats;
1260        if fixed && s.settled_blocks > 0 {
1261            if let Some(state) = self.states.get_mut(&raw.layer) {
1262                state.settled += 1;
1263            }
1264        }
1265        let canvas = paint.p.canvas.clone();
1266        if let Some(meta) = self.metas.get_mut(&canvas) {
1267            let placed_rect = overscroll::trim(s.bounds, left_out);
1268            if placed_rect.width > 0.0 {
1269                meta.bounds = union(meta.bounds, placed_rect);
1270            }
1271            meta.observed_pixels += s.added as f64;
1272            meta.uncertain_pixels += s.uncertain as f64;
1273            meta.conflict_pixels += s.conflicts as f64;
1274            meta.tile_count += s.tiles as f64;
1275            meta.provisional_pixels += s.provisional_pixels as f64;
1276            meta.last_time = raw.time;
1277        }
1278        self.counters.tiles += s.tiles as f64;
1279        self.counters.observed_pixels += s.added as f64;
1280        self.mark_dirty(&canvas);
1281        if moving {
1282            job.latest = Some(canvas.clone());
1283        }
1284        job.decisions.push(decision(
1285            canvas,
1286            placed(raw, &paint.p, strips),
1287            Some((s.added as f64, s.conflicts as f64, s.uncertain as f64)),
1288        ));
1289        Ok(None)
1290    }
1291
1292    /// The lines a moving-region placement leaves out past its canvas's page edges (none for other regions),
1293    /// working the edges out from the plans the host handed over on first use.
1294    fn left_out(
1295        &mut self,
1296        raw: &PlanPlacement,
1297        paint: &Paint,
1298        region_at: usize,
1299        current: usize,
1300        io: &RenderIo,
1301    ) -> Result<[i64; 4], RenderError> {
1302        let graph = io.graph;
1303        let region = &self.regions[region_at];
1304        if region.kind != RegionKind::Moving {
1305            return Ok([0; 4]);
1306        }
1307        let (regions, index_of) = (&self.regions, &self.region_index);
1308        let (attachments, overscroll) = (&self.attachments, &mut self.overscroll);
1309        overscroll.prepare(
1310            // A placement that cannot be resolved is left out of the trajectory; its own frame fails as before.
1311            |p, frame| {
1312                let resolved = graph
1313                    .correction(&p.node, frame as f64)
1314                    .and_then(|(cx, cy)| {
1315                        let (sx, sy) = render_shift(attachments, &p.canvas_id, graph)?;
1316                        Ok((p.x + cx + sx, p.y + cy + sy))
1317                    });
1318                let canvas = resolve_target(attachments, &p.canvas_id).to_string();
1319                Ok::<_, RenderError>(
1320                    resolved
1321                        .ok()
1322                        .filter(|&(x, y)| is_valid_pose(x, y))
1323                        .map(|(x, y)| (canvas, x, y)),
1324                )
1325            },
1326            |layer| {
1327                let r = regions[*index_of.get(layer)?].rect;
1328                let (_, _, w, h) = overscroll::pixel_rect(r);
1329                Some((w, h))
1330            },
1331        )?;
1332        // Neighbours whose page moved against this frame show what stays put on screen.
1333        let moved = |n: &Option<NeighbourPlacement>| {
1334            n.as_ref().and_then(|n| {
1335                let same = n.resolved.canvas == paint.p.canvas
1336                    && js_round(n.resolved.x) == js_round(paint.p.x)
1337                    && js_round(n.resolved.y) == js_round(paint.p.y);
1338                (!same).then_some(n.slot).flatten()
1339            })
1340        };
1341        let slots = paint
1342            .neighbours
1343            .as_ref()
1344            .map_or([None, None], |[a, b]| [moved(a), moved(b)]);
1345        let ring = &self.ring;
1346        let lines = crate::kernels::overscroll::Lines {
1347            rgba: &ring.slots[current].pixels,
1348            width: self.width,
1349            height: self.height,
1350            labels: Some((io.labels, region.code)),
1351            region: overscroll::pixel_rect(region.rect),
1352            neighbours: slots.map(|s| s.map(|s| &ring.slots[s].pixels[..])),
1353            noise: self.noise,
1354        };
1355        let at = overscroll::Resolved {
1356            canvas: &paint.p.canvas,
1357            x: paint.p.x,
1358            y: paint.p.y,
1359        };
1360        Ok(self.overscroll.measure(&raw.layer, at, &lines))
1361    }
1362
1363    /// The previous and next frames' placements of the same layer, resolved, when that frame is in hand and placed
1364    /// the layer at all; a neighbour whose resolved pose cannot be placed counts as absent.
1365    fn neighbours(
1366        &self,
1367        job: &FrameJob,
1368        raw: &PlanPlacement,
1369        graph: &PoseGraph,
1370    ) -> Result<[Option<NeighbourPlacement>; 2], RenderError> {
1371        let input = &job.input;
1372        fn find<'a>(
1373            has: bool,
1374            plan: &'a Option<FramePlan>,
1375            layer: &str,
1376        ) -> Option<&'a PlanPlacement> {
1377            if !has {
1378                return None;
1379            }
1380            plan.as_ref()?
1381                .placements
1382                .iter()
1383                .find(|pl| pl.layer == layer && !pl.skipped())
1384        }
1385        let prev = find(
1386            input.has_prev && input.index > 0,
1387            &input.prev_plan,
1388            &raw.layer,
1389        );
1390        let next = find(input.has_next, &input.next_plan, &raw.layer);
1391        let prev_resolved = match prev {
1392            Some(pl) => Some((self.resolve(pl, input.index as f64 - 1.0, graph)?, pl)),
1393            None => None,
1394        };
1395        let next_resolved = match next {
1396            Some(pl) => Some((self.resolve(pl, input.index as f64 + 1.0, graph)?, pl)),
1397            None => None,
1398        };
1399        let keep = |r: Option<(Resolved, &PlanPlacement)>| {
1400            r.filter(|(p, _)| is_valid_pose(p.x, p.y))
1401                .map(|(resolved, pl)| NeighbourPlacement {
1402                    resolved,
1403                    occlusions: pl.occlusions.clone().unwrap_or_default(),
1404                    slot: None,
1405                })
1406        };
1407        Ok([keep(prev_resolved), keep(next_resolved)])
1408    }
1409}
1410
1411/// One of the compositor's findings as a structured diagnostic.
1412fn compositor_diagnostic(d: CompositorDiagnostic) -> Diagnostic {
1413    match d {
1414        CompositorDiagnostic::IncompletePatch {
1415            canvas,
1416            frame,
1417            time,
1418            region,
1419        } => Diagnostic::new("INCOMPLETE_TEMPORAL_PATCH", Severity::Warning)
1420            .at(frame, time)
1421            .canvas(&canvas)
1422            .region(region),
1423        CompositorDiagnostic::Conflict {
1424            canvas,
1425            frame,
1426            time,
1427            region,
1428            confidence,
1429            regions,
1430            conflicts,
1431        } => Diagnostic::new("TEMPORAL_OR_ALIGNMENT_CONFLICT", Severity::Warning)
1432            .at(frame, time)
1433            .param("conflicts", conflicts as f64)
1434            .canvas(&canvas)
1435            .region(region)
1436            .confidence(confidence)
1437            .detail(Value::Object(vec![(
1438                "regions",
1439                Value::List(regions.into_iter().map(Value::rect).collect()),
1440            )])),
1441    }
1442}
1443
1444/// The voting records a frame asks the window for, in order.
1445fn vote_gets(index: u32, has_prev: bool, has_next: bool) -> Vec<i64> {
1446    let index = index as i64;
1447    let mut gets = vec![index];
1448    if has_prev {
1449        gets.push(index - 1);
1450    }
1451    if has_next {
1452        gets.push(index + 1);
1453    }
1454    gets
1455}
1456
1457/// The region's verdict from a record, checked against its box.
1458fn vote<'a>(
1459    record: Option<&'a ConsistencyRecord>,
1460    region: &str,
1461) -> Result<Option<Vote<'a>>, RenderError> {
1462    let Some(v) = record.and_then(|r| r.get(region)) else {
1463        return Ok(None);
1464    };
1465    if v.w <= 0 || v.h <= 0 {
1466        return Err(RenderError::BadVote);
1467    }
1468    let bytes = (v.w as usize * v.h as usize).div_ceil(8);
1469    if v.bits.len() < bytes || v.clean.len() < bytes {
1470        return Err(RenderError::BadVote);
1471    }
1472    Ok(Some(Vote {
1473        x0: v.x0,
1474        y0: v.y0,
1475        w: v.w,
1476        h: v.h,
1477        bits: &v.bits[..bytes],
1478        clean: &v.clean[..bytes],
1479    }))
1480}
1481
1482/// The plan's placement at its resolved pose and canvas, with `left_out` (lines past the page's edges) added to its
1483/// occlusions.
1484fn placed(raw: &PlanPlacement, p: &Resolved, left_out: Vec<Rect>) -> PlanPlacement {
1485    let mut occlusions = raw.occlusions.clone();
1486    if !left_out.is_empty() {
1487        occlusions.get_or_insert_with(Vec::new).extend(left_out);
1488    }
1489    PlanPlacement {
1490        canvas_id: p.canvas.clone(),
1491        x: p.x,
1492        y: p.y,
1493        occlusions,
1494        ..raw.clone()
1495    }
1496}
1497
1498/// A ledger row; `None` pixels means a skipped placement.
1499fn decision(
1500    canvas_id: String,
1501    placement: PlanPlacement,
1502    pixels: Option<(f64, f64, f64)>,
1503) -> RenderDecision {
1504    let (added, conflicts, uncertain) = pixels.unwrap_or_default();
1505    RenderDecision {
1506        canvas_id,
1507        placement,
1508        added_pixels: added,
1509        conflict_pixels: conflicts,
1510        uncertain_pixels: uncertain,
1511        skipped: pixels.is_none(),
1512        reused_exact_observation: false,
1513    }
1514}
1515
1516/// The smallest rect holding both; an empty `a` is replaced by `b`.
1517fn union(a: Rect, b: Rect) -> Rect {
1518    if a.width <= 0.0 || a.height <= 0.0 {
1519        return b;
1520    }
1521    let (x, y) = (a.x.min(b.x), a.y.min(b.y));
1522    Rect {
1523        x,
1524        y,
1525        width: (a.x + a.width).max(b.x + b.width) - x,
1526        height: (a.y + a.height).max(b.y + b.height) - y,
1527    }
1528}
1529
1530#[cfg(test)]
1531mod fixed_tests;
1532#[cfg(test)]
1533mod tests;